Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ services:
environment:
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- GRPC_PORT=${GRPC_PORT:-50051}
- CRYPTO_AUTH_TOKEN=${CRYPTO_AUTH_TOKEN:-}
- CRYPTO_TLS_CERT_PATH=${CRYPTO_TLS_CERT_PATH:-none}
- CRYPTO_TLS_KEY_PATH=${CRYPTO_TLS_KEY_PATH:-none}
volumes:
- crypto_certs:/data/crypto
healthcheck:
test: ["CMD-SHELL", "nc -z localhost ${GRPC_PORT:-50051} || exit 1"]
interval: 10s
Expand All @@ -36,6 +41,8 @@ services:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL:-postgresql://keyway:keyway@db:5432/keyway}
- CRYPTO_SERVICE_URL=${CRYPTO_SERVICE_URL:-crypto:50051}
- CRYPTO_AUTH_TOKEN=${CRYPTO_AUTH_TOKEN:-}
- CRYPTO_TLS_CA_PATH=${CRYPTO_TLS_CA_PATH:-}
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- JWT_SECRET=${JWT_SECRET}
# GitHub App (unified auth + repo access)
Expand All @@ -60,6 +67,8 @@ services:
# Optional: Analytics
- POSTHOG_API_KEY=${POSTHOG_API_KEY:-}
- POSTHOG_HOST=${POSTHOG_HOST:-}
volumes:
- crypto_certs:/data/crypto:ro
depends_on:
db:
condition: service_healthy
Expand Down Expand Up @@ -103,3 +112,4 @@ volumes:
postgres_data:
caddy_data:
caddy_config:
crypto_certs:
22 changes: 22 additions & 0 deletions packages/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,28 @@ DATABASE_URL=postgresql://user:password@localhost:5432/keyway
# The encryption key is stored in keyway-crypto, NOT here
CRYPTO_SERVICE_URL=localhost:50051

# Security: shared secret for crypto service authentication (optional but recommended)
# Must match CRYPTO_AUTH_TOKEN on the crypto service.
# Generate with: openssl rand -hex 32
# CRYPTO_AUTH_TOKEN=
#
# TLS for crypto service (optional - for encrypted gRPC transport)
# The crypto service auto-generates a self-signed ECDSA P-256 cert on first start.
#
# Deployment recommendations:
# - Docker Compose: use the shared volume (cert auto-distributed):
# CRYPTO_TLS_CA_PATH=/data/crypto/tls.crt
# - Railway: TLS is not needed (private networking is already isolated).
# Just use CRYPTO_AUTH_TOKEN for authentication. The .railway.internal
# network is not shared with other tenants.
# - Kubernetes: mount the cert via a Secret or shared volume:
# CRYPTO_TLS_CA_PATH=/etc/keyway/crypto-tls/tls.crt
# - Other (env-based): pass the cert PEM as base64:
# CRYPTO_TLS_CA=LS0tLS1CRUdJTi...
#
# CRYPTO_TLS_CA_PATH=
# CRYPTO_TLS_CA=

# -----------------------------------------
# JWT (for device flow tokens)
# -----------------------------------------
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ const envSchema = z

// Encryption (remote crypto service)
CRYPTO_SERVICE_URL: z.string().min(1, "CRYPTO_SERVICE_URL is required (e.g., localhost:50051)"),
CRYPTO_AUTH_TOKEN: z.preprocess(emptyToUndefined, z.string().optional()),
CRYPTO_TLS_CA: z.preprocess(emptyToUndefined, z.string().optional()),
CRYPTO_TLS_CA_PATH: z.preprocess(emptyToUndefined, z.string().optional()),

// JWT for Keyway tokens
JWT_SECRET: z.string().min(32, "JWT_SECRET must be at least 32 characters"),
Expand Down Expand Up @@ -167,6 +170,9 @@ export const config = {

crypto: {
serviceUrl: env.CRYPTO_SERVICE_URL,
authToken: env.CRYPTO_AUTH_TOKEN,
tlsCa: env.CRYPTO_TLS_CA,
tlsCaPath: env.CRYPTO_TLS_CA_PATH,
},

jwt: {
Expand Down
12 changes: 10 additions & 2 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ fastify.get("/health", async (request, reply) => {

// Check crypto service connectivity with a 3s timeout
try {
const health = await withTimeout(checkCryptoService(config.crypto.serviceUrl), 3000);
const health = await withTimeout(checkCryptoService(config.crypto.serviceUrl, {
authToken: config.crypto.authToken,
tlsCa: config.crypto.tlsCa,
tlsCaPath: config.crypto.tlsCaPath,
}), 3000);
cryptoVersion = health.version;
} catch {
cryptoStatus = "disconnected";
Expand Down Expand Up @@ -271,7 +275,11 @@ const start = async () => {
// Check crypto service connectivity (warning only, don't crash)
fastify.log.info(`Checking crypto service connectivity at ${config.crypto.serviceUrl}...`);
try {
const health = await checkCryptoService(config.crypto.serviceUrl);
const health = await checkCryptoService(config.crypto.serviceUrl, {
authToken: config.crypto.authToken,
tlsCa: config.crypto.tlsCa,
tlsCaPath: config.crypto.tlsCaPath,
});
fastify.log.info({ version: health.version }, "Crypto service connection successful");
} catch (cryptoError) {
fastify.log.warn(
Expand Down
6 changes: 5 additions & 1 deletion packages/backend/src/utils/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ let encryptionService: IEncryptionService | null = null;
export async function getEncryptionService(): Promise<IEncryptionService> {
if (!encryptionService) {
const { RemoteEncryptionService } = await import("./remoteEncryption.js");
encryptionService = new RemoteEncryptionService(config.crypto.serviceUrl);
encryptionService = new RemoteEncryptionService(config.crypto.serviceUrl, {
authToken: config.crypto.authToken,
tlsCa: config.crypto.tlsCa,
tlsCaPath: config.crypto.tlsCaPath,
});
logger.info({ serviceUrl: config.crypto.serviceUrl }, "Using remote encryption service");
}
return encryptionService;
Expand Down
Loading
Loading