Skip to content

feat(crypto): add TLS + shared-secret auth to gRPC service - #14

Merged
NicolasRitouet merged 7 commits into
mainfrom
security/crypto-grpc-auth-tls
Apr 9, 2026
Merged

feat(crypto): add TLS + shared-secret auth to gRPC service#14
NicolasRitouet merged 7 commits into
mainfrom
security/crypto-grpc-auth-tls

Conversation

@NicolasRitouet

@NicolasRitouet NicolasRitouet commented Apr 9, 2026

Copy link
Copy Markdown
Member

Summary

  • Add shared-secret gRPC interceptor with SHA-256 + constant-time comparison to authenticate backend→crypto calls
  • Add auto-generated self-signed ECDSA P-256 TLS cert for encrypted gRPC transport
  • Both layers are opt-in via env vars — zero breaking changes for existing deployments
  • Fix audit finding V22: remove ENV ENCRYPTION_KEY="" from Dockerfile

Context

The crypto gRPC service is the single point of cryptographic trust in Keyway — it encrypts/decrypts all secrets via AES-256-GCM. While network isolation (Docker/Railway) prevents external access, a compromised container on the same network could call Encrypt/Decrypt without authentication. This PR adds defense-in-depth.

What changed

Go crypto service (packages/crypto/main.go)

  • authInterceptor: validates x-crypto-auth-token metadata header. Uses SHA-256 hashing before constant-time comparison to prevent both timing and token-length leaking. gRPC health probes (/grpc.health.v1.Health/Check) are exempt so Docker/k8s healthchecks work.
  • loadOrGenerateTLS: loads existing cert/key or generates self-signed ECDSA P-256 with SANs [localhost, 127.0.0.1, crypto, *.railway.internal]. Detects mismatched cert/key files and errors rather than silently regenerating.
  • CRYPTO_TLS_REQUIRED=true makes TLS failures fatal (recommended for production).

TypeScript backend client (packages/backend/src/utils/remoteEncryption.ts)

  • Accepts CryptoServiceOptions (authToken, tlsCa, tlsCaPath)
  • Sends auth token via gRPC metadata on every RPC call
  • TLS mode: uses CA cert PEM as trust anchor (grpc.credentials.createSsl(rootCerts))
  • Insecure mode: validates trusted network (existing behavior, unchanged)
  • checkCryptoService() propagates options for startup health checks

Docker Compose

  • Shared crypto_certs volume (rw for crypto, ro for backend)
  • Healthcheck verifies cert file exists before reporting healthy
  • New env vars documented in .env.example

New environment variables (all optional)

Variable Service Purpose
CRYPTO_AUTH_TOKEN crypto + backend Shared secret for gRPC auth
CRYPTO_TLS_CERT_PATH crypto Path to TLS cert (default: /data/crypto/tls.crt, none to disable)
CRYPTO_TLS_KEY_PATH crypto Path to TLS key (default: /data/crypto/tls.key)
CRYPTO_TLS_REQUIRED crypto true = fatal on TLS failure
CRYPTO_TLS_CA_PATH backend Path to CA cert for TLS verification (Docker volume)
CRYPTO_TLS_CA backend Base64-encoded CA cert PEM (Railway/env-based)

Deployment modes

  1. No config (default): current behavior, warnings in logs
  2. Auth only: set CRYPTO_AUTH_TOKEN on both services
  3. Auth + TLS (recommended): set CRYPTO_AUTH_TOKEN + TLS cert path/volume

Test plan

  • 144 Go tests pass (133 existing + 11 new) with -race
  • 923 backend tests pass (all mocked, no changes needed)
  • TypeScript type-check passes
  • Manual: docker compose up --build without new env vars (backward compat)
  • Manual: docker compose up --build with CRYPTO_AUTH_TOKEN set
  • Manual: docker compose up --build with TLS + auth configured

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Crypto service: shared-token authentication and optional TLS with auto-generated certificates.
  • Infrastructure

    • Compose updated to persist/share TLS artifacts between services and support secure connections.
  • Refactor

    • Dashboard components now import from per-component entry points; central barrel export removed.
  • Tests

    • Added comprehensive auth and TLS tests for the crypto service.
  • Chores

    • Example environment docs and TypeScript JSX config updated; crypto README clarified.

NicolasRitouet and others added 4 commits March 26, 2026 01:12
…alytics

- Remove barrel file (index.ts with 25+ re-exports), replace with
  direct imports across 18 files for better tree-shaking
- Lazy-load 6 modals with next/dynamic (SecretModal, ViewSecretModal,
  DeleteVaultModal, NewVaultModal, BulkImportModal, ConnectOrgModal)
- Change PostHog script strategy from afterInteractive to lazyOnload

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The two separate Script tags with strategy="lazyOnload" had no
guaranteed execution order, so the init script could run before the
library was loaded. Merge into a single Script with onLoad callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rrel

The barrel file was removed but 9 test files still mocked the old
barrel path. Update mocks to target individual component files,
fixing all 89 failing tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Defense-in-depth for the crypto gRPC service that encrypts/decrypts
all secrets in the platform. Adds two opt-in security layers:

- Shared-secret interceptor: validates x-crypto-auth-token metadata
  header using SHA-256 + constant-time comparison (prevents timing
  and length-leaking attacks). gRPC health probes are exempt.
- TLS with auto-generated self-signed ECDSA P-256 cert: encrypts
  traffic between backend and crypto service. Cert is generated at
  first startup and persisted via Docker volume. Backend verifies
  via cert pinning (PEM as rootCerts).

Both layers are backward-compatible: without env vars configured,
the service runs in the current insecure mode with log warnings.
CRYPTO_TLS_REQUIRED=true makes TLS failures fatal for production.

Also fixes audit finding V22: removes ENV ENCRYPTION_KEY="" from
the Dockerfile (was leaking in docker inspect).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
keyway-dashboard Ready Ready Preview, Comment Apr 9, 2026 8:18pm

Request Review

@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds token-based gRPC authentication and optional TLS (with self-signed cert bootstrapping) to the crypto service; backend is updated to pass auth/TLS options to the crypto client and health checks; docker-compose and env examples share TLS assets; dashboard barrel exports removed and many imports switched to granular paths with some modals lazy-loaded.

Changes

Cohort / File(s) Summary
Crypto service (auth & TLS)
packages/crypto/main.go, packages/crypto/auth_test.go, packages/crypto/tls_test.go
Added authInterceptor enforcing x-crypto-auth-token (constant-time SHA-256 check) with health check exemption; added loadOrGenerateTLS to create/validate ECDSA P-256 certs and fingerprint; tests cover auth and TLS behaviors.
Crypto image setup
packages/crypto/Dockerfile, docker-compose.yml
Dockerfile creates /data/crypto and sets ownership; compose adds crypto_certs volume and mounts it into crypto and backend services (backend mount is read-only).
Backend config & wiring
packages/backend/src/config/index.ts, packages/backend/.env.example, packages/backend/src/index.ts
Added env-backed config entries CRYPTO_AUTH_TOKEN, CRYPTO_TLS_CA, CRYPTO_TLS_CA_PATH (placeholders in .env.example); health checks and startup connectivity checks now pass authToken, tlsCa, tlsCaPath to crypto checks.
Remote crypto client
packages/backend/src/utils/remoteEncryption.ts, packages/backend/src/utils/encryption.ts
Introduced CryptoServiceOptions (authToken, tlsCa, tlsCaPath); RemoteEncryptionService accepts options, attaches auth metadata, selects TLS (createSsl) when CA provided or insecure with trusted-network validation otherwise; added callRpc() helper and checkCryptoService(..., options?).
Dashboard import refactor & dynamic loading
packages/dashboard/app/components/dashboard/index.ts, packages/dashboard/app/(dashboard)/**/page.tsx, packages/dashboard/app/(dashboard)/**/_components/*.tsx, packages/dashboard/app/(dashboard)/vaults/.../page.tsx
Removed dashboard barrel re-exports; updated pages/components to import from granular component paths (e.g., .../Layout, .../ErrorState); several modals switched to next/dynamic lazy loading (ssr: false).
Dashboard tests updated
packages/dashboard/tests/**/*.test.tsx
Adjusted many tests to mock the new per-component module paths (e.g., .../Layout, .../ErrorState, .../ExposureStatCard); added a few test-specific mocks like usePathname and getOrganizations.
Dashboard build/runtime tweaks
packages/dashboard/layout.tsx, packages/dashboard/tsconfig.json, packages/dashboard/next-env.d.ts
Consolidated PostHog init into single Script with lazyOnload + onLoad; changed TS JSX setting to react-jsx; replaced triple-slash route ref with an ES module import.
Docs
packages/crypto/README.md
Documented new env vars: CRYPTO_AUTH_TOKEN, CRYPTO_TLS_CERT_PATH, CRYPTO_TLS_KEY_PATH, CRYPTO_TLS_REQUIRED and updated security/TLS guidance.

Sequence Diagram

sequenceDiagram
    participant Backend as Backend Service
    participant RemoteClient as RemoteEncryptionService (client)
    participant CryptoServer as Crypto gRPC Server
    participant Auth as Auth Interceptor
    participant TLS as TLS Layer

    Backend->>RemoteClient: invoke RPC (options: authToken, tlsCa/tlsCaPath)
    RemoteClient->>CryptoServer: open gRPC channel (with Metadata x-crypto-auth-token) / TLS handshake
    CryptoServer->>Auth: authInterceptor intercepts unary call
    Auth->>Auth: compute SHA-256, constant-time compare
    alt token valid
        Auth->>TLS: allow to proceed
        TLS->>TLS: verify client TLS using CA (if provided)
        alt TLS verified or not required
            CryptoServer->>CryptoServer: execute RPC method
            CryptoServer-->>RemoteClient: RPC response
            RemoteClient-->>Backend: return result
        else TLS verification failed
            CryptoServer-->>RemoteClient: TLS error
            RemoteClient-->>Backend: propagate TLS error
        end
    else token invalid/missing
        Auth-->>RemoteClient: return Unauthenticated
        RemoteClient-->>Backend: propagate Unauthenticated
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped in with a secret key,

tokens hashed and kept with care,
certs that sprout when none are there,
backend and compose now share the lair.
Imports trimmed and modals light—code leaps, carrots, and delight! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main feature: adding TLS and shared-secret authentication to the gRPC service, which aligns with the comprehensive changes across crypto service, backend client, and deployment configuration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/crypto-grpc-auth-tls

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Add curly braces to if-return (curly rule)
- Replace `Function` type with explicit `(...args: unknown[]) => void`
  to satisfy @typescript-eslint/no-unsafe-function-type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
packages/dashboard/app/layout.tsx (1)

60-61: Avoid @ts-expect-error for window.posthog.init

Line 60 suppresses typing rather than modeling the API. Prefer extending the global window.posthog type (e.g., include init) and removing the suppression to keep type checks meaningful.

Type-safe cleanup
-              // `@ts-expect-error` -- posthog is loaded via external script
-              window.posthog?.init(posthogKey, {
+              window.posthog?.init?.(posthogKey, {
                 api_host: posthogHost,
// packages/dashboard/lib/analytics.ts (or a dedicated global.d.ts)
declare global {
  interface Window {
    posthog?: {
      init?: (key: string, config?: Record<string, unknown>) => void
      capture: (event: string, properties?: Record<string, unknown>) => void
      identify: (distinctId: string, properties?: Record<string, unknown>) => void
    }
  }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dashboard/app/layout.tsx` around lines 60 - 61, The code currently
suppresses TypeScript with "// `@ts-expect-error`" before calling
window.posthog?.init; instead add a proper global type for window.posthog
(including init, capture, identify signatures) in a declaration file (e.g.,
packages/dashboard/lib/analytics.ts or a new global.d.ts) and then remove the
ts-expect-error and call window.posthog?.init normally; ensure the declaration
defines Window.posthog with init?: (key: string, config?: Record<string,
unknown>) => void and the other methods referenced so TypeScript knows the API
used by layout.tsx.
packages/crypto/auth_test.go (1)

193-236: Consider extracting common server setup to reduce duplication.

This test duplicates most of the setup logic from setupAuthTestServer. You could extend the helper to optionally register the health server:

func setupAuthTestServer(t *testing.T, token string, keys map[uint32]string, registerHealth bool) (...)

That said, the explicit inline setup makes the test self-contained and clear about what services are registered, so this is a minor refinement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/crypto/auth_test.go` around lines 193 - 236, The test
TestAuthInterceptor_GrpcHealthExempt duplicates server setup logic; refactor by
extending setupAuthTestServer to accept an optional registerHealth bool (e.g.,
func setupAuthTestServer(t *testing.T, token string, keys map[uint32]string,
registerHealth bool) (...)) so callers can request the health server be
registered, then update TestAuthInterceptor_GrpcHealthExempt to call
setupAuthTestServer with registerHealth=true and remove the in-test server
creation/registration code, ensuring authInterceptor,
pb.RegisterCryptoServiceServer, and grpc_health_v1.RegisterHealthServer are
handled by the helper.
packages/backend/src/utils/remoteEncryption.ts (1)

189-198: Consider adding basic PEM validation for clearer error messages.

If tlsCa contains invalid base64 or tlsCaPath points to a non-PEM file, the error will surface during the TLS handshake with a less informative message. A simple sanity check could improve debuggability:

💡 Optional improvement
 private loadTlsCa(options?: CryptoServiceOptions): Buffer | null {
   if (options?.tlsCa) {
-    return Buffer.from(options.tlsCa, "base64");
+    const pem = Buffer.from(options.tlsCa, "base64");
+    if (!pem.toString().includes("-----BEGIN")) {
+      throw new Error("CRYPTO_TLS_CA does not appear to be valid base64-encoded PEM");
+    }
+    return pem;
   }
   if (options?.tlsCaPath) {
     // Path was explicitly configured -- fail loudly if unreadable
-    return fs.readFileSync(options.tlsCaPath);
+    const pem = fs.readFileSync(options.tlsCaPath);
+    if (!pem.toString().includes("-----BEGIN")) {
+      throw new Error(`File at CRYPTO_TLS_CA_PATH (${options.tlsCaPath}) does not appear to be valid PEM`);
+    }
+    return pem;
   }
   return null;
 }

This is optional since the TLS layer will still reject invalid certs, just with less context.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/utils/remoteEncryption.ts` around lines 189 - 198, The
loadTlsCa method should perform basic PEM and base64 validation to give clearer
errors: when options.tlsCa is provided, attempt to decode and verify it looks
like a PEM certificate (e.g., contains "-----BEGIN CERTIFICATE-----" / "-----END
CERTIFICATE-----") and throw a descriptive error if base64 decode fails or
markers are missing; when options.tlsCaPath is provided, read the file and
validate its contents similarly, and if unreadable or not PEM-like throw a clear
error mentioning tlsCaPath; update loadTlsCa to return the Buffer only after
these sanity checks so callers see precise validation failures instead of TLS
handshake errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docker-compose.yml`:
- Around line 23-25: The compose defaults cause mismatched TLS behavior because
CRYPTO_TLS_CERT_PATH being empty triggers certificate generation in
packages/crypto/main.go while the backend only enables TLS when CRYPTO_TLS_CA or
CRYPTO_TLS_CA_PATH is set; fix by explicitly defaulting the crypto service cert
path to a sentinel value (e.g. set CRYPTO_TLS_CERT_PATH=none) so an empty docker
compose run means TLS off, and make the same change for the duplicate env
entries (the other CRYPTO_TLS_CERT_PATH lines around 44-45); update any startup
logic that checks CRYPTO_TLS_CERT_PATH to treat "none" as disabling TLS,
referencing CRYPTO_TLS_CERT_PATH, CRYPTO_TLS_CA, CRYPTO_TLS_CA_PATH and
packages/crypto/main.go to locate the relevant checks.
- Line 29: The healthcheck currently hardcodes /data/crypto/tls.crt and fails
when CRYPTO_TLS_CERT_PATH is set to "none" or a custom path; update the
healthcheck command in docker-compose.yml (the test line that references
CRYPTO_TLS_CERT_PATH and GRPC_PORT) to: 1) treat CRYPTO_TLS_CERT_PATH="none" as
valid (skip file existence check), 2) if CRYPTO_TLS_CERT_PATH is non-empty and
not "none" check that that file exists, and 3) if CRYPTO_TLS_CERT_PATH is empty
fall back to the default /data/crypto/tls.crt; keep the existing port check (nc
-z localhost ${GRPC_PORT:-50051}) and exit nonzero on failure.

In `@packages/crypto/main.go`:
- Around line 121-128: When reusing the loaded keypair in the certExists branch
(after tls.LoadX509KeyPair returns a cert), parse the leaf bytes in
cert.Certificate[0] with x509.ParseCertificate and validate its
NotBefore/NotAfter against time.Now(); if now is outside that validity window,
do not return the stale cert and instead trigger regeneration (or return an
explicit error) so rotation happens instead of continuing to use an
expired/not-yet-valid certificate; keep the current SHA-256 fingerprint logic
(sha256.Sum256 and hex.EncodeToString) only for valid certificates.
- Around line 158-176: The current implementation writes certPEM to certPath and
keyPEM to keyPath directly, which can leave a half-written pair on disk; modify
the write logic to write both artifacts to temporary files (e.g.,
certPath+".tmp" and keyPath+".tmp" or use os.CreateTemp), fsync/close them, then
atomically rename (os.Rename) them to certPath and keyPath only after both
writes succeed; on any error during marshal/write/load (including before
tls.LoadX509KeyPair), remove any temp files and avoid leaving partial files, and
ensure file permissions for the key are set to 0600 before rename; update the
code paths around certPEM/keyPEM writes and tls.LoadX509KeyPair to use the final
paths so mismatched TLS files cannot be left behind.

In `@packages/dashboard/app/layout.tsx`:
- Around line 58-69: The PostHog init in layout.tsx uses strategy="lazyOnload"
causing a race where trackEvent calls in login/page.tsx and
auth/callback/page.tsx can be lost; change the script loading strategy to
"afterInteractive" so window.posthog is available on mount, or implement an
event queue (e.g., push events to a temporary array and flush them in the init
callback inside the onLoad closure) to capture early events; also remove the
unnecessary // `@ts-expect-error` before window.posthog since analytics.ts already
types window.posthog as optional.

---

Nitpick comments:
In `@packages/backend/src/utils/remoteEncryption.ts`:
- Around line 189-198: The loadTlsCa method should perform basic PEM and base64
validation to give clearer errors: when options.tlsCa is provided, attempt to
decode and verify it looks like a PEM certificate (e.g., contains "-----BEGIN
CERTIFICATE-----" / "-----END CERTIFICATE-----") and throw a descriptive error
if base64 decode fails or markers are missing; when options.tlsCaPath is
provided, read the file and validate its contents similarly, and if unreadable
or not PEM-like throw a clear error mentioning tlsCaPath; update loadTlsCa to
return the Buffer only after these sanity checks so callers see precise
validation failures instead of TLS handshake errors.

In `@packages/crypto/auth_test.go`:
- Around line 193-236: The test TestAuthInterceptor_GrpcHealthExempt duplicates
server setup logic; refactor by extending setupAuthTestServer to accept an
optional registerHealth bool (e.g., func setupAuthTestServer(t *testing.T, token
string, keys map[uint32]string, registerHealth bool) (...)) so callers can
request the health server be registered, then update
TestAuthInterceptor_GrpcHealthExempt to call setupAuthTestServer with
registerHealth=true and remove the in-test server creation/registration code,
ensuring authInterceptor, pb.RegisterCryptoServiceServer, and
grpc_health_v1.RegisterHealthServer are handled by the helper.

In `@packages/dashboard/app/layout.tsx`:
- Around line 60-61: The code currently suppresses TypeScript with "//
`@ts-expect-error`" before calling window.posthog?.init; instead add a proper
global type for window.posthog (including init, capture, identify signatures) in
a declaration file (e.g., packages/dashboard/lib/analytics.ts or a new
global.d.ts) and then remove the ts-expect-error and call window.posthog?.init
normally; ensure the declaration defines Window.posthog with init?: (key:
string, config?: Record<string, unknown>) => void and the other methods
referenced so TypeScript knows the API used by layout.tsx.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d3334673-ebca-492c-973c-264502ad23b3

📥 Commits

Reviewing files that changed from the base of the PR and between c49e422 and 9d25ef0.

📒 Files selected for processing (41)
  • docker-compose.yml
  • packages/backend/.env.example
  • packages/backend/src/config/index.ts
  • packages/backend/src/index.ts
  • packages/backend/src/utils/encryption.ts
  • packages/backend/src/utils/remoteEncryption.ts
  • packages/crypto/Dockerfile
  • packages/crypto/auth_test.go
  • packages/crypto/main.go
  • packages/crypto/tls_test.go
  • packages/dashboard/app/(dashboard)/activity/page.tsx
  • packages/dashboard/app/(dashboard)/api-keys/page.tsx
  • packages/dashboard/app/(dashboard)/orgs/[org]/billing/page.tsx
  • packages/dashboard/app/(dashboard)/orgs/[org]/exposure/page.tsx
  • packages/dashboard/app/(dashboard)/orgs/[org]/members/page.tsx
  • packages/dashboard/app/(dashboard)/orgs/[org]/page.tsx
  • packages/dashboard/app/(dashboard)/orgs/[org]/settings/page.tsx
  • packages/dashboard/app/(dashboard)/orgs/page.tsx
  • packages/dashboard/app/(dashboard)/page.tsx
  • packages/dashboard/app/(dashboard)/security/_components/SecurityAccessLogTab.tsx
  • packages/dashboard/app/(dashboard)/security/_components/SecurityAlertsTab.tsx
  • packages/dashboard/app/(dashboard)/security/_components/SecurityExposureTab.tsx
  • packages/dashboard/app/(dashboard)/security/_components/SecurityOverviewTab.tsx
  • packages/dashboard/app/(dashboard)/security/page.tsx
  • packages/dashboard/app/(dashboard)/settings/page.tsx
  • packages/dashboard/app/(dashboard)/vaults/[owner]/[repo]/collaborators/page.tsx
  • packages/dashboard/app/(dashboard)/vaults/[owner]/[repo]/environments/page.tsx
  • packages/dashboard/app/(dashboard)/vaults/[owner]/[repo]/page.tsx
  • packages/dashboard/app/components/dashboard/index.ts
  • packages/dashboard/app/layout.tsx
  • packages/dashboard/next-env.d.ts
  • packages/dashboard/tests/components/SecurityAccessLogTab.test.tsx
  • packages/dashboard/tests/components/SecurityAlertsTab.test.tsx
  • packages/dashboard/tests/components/SecurityExposureTab.test.tsx
  • packages/dashboard/tests/components/SecurityOverviewTab.test.tsx
  • packages/dashboard/tests/pages/ActivityPage.test.tsx
  • packages/dashboard/tests/pages/ApiKeysPage.test.tsx
  • packages/dashboard/tests/pages/SecurityPage.test.tsx
  • packages/dashboard/tests/pages/SettingsPage.test.tsx
  • packages/dashboard/tests/pages/VaultDetailPage.test.tsx
  • packages/dashboard/tsconfig.json
💤 Files with no reviewable changes (1)
  • packages/dashboard/app/components/dashboard/index.ts

Comment thread docker-compose.yml Outdated
Comment thread docker-compose.yml Outdated
- crypto_certs:/data/crypto
healthcheck:
test: ["CMD-SHELL", "nc -z localhost ${GRPC_PORT:-50051} || exit 1"]
test: ["CMD-SHELL", "(test -z \"${CRYPTO_TLS_CERT_PATH}\" || test -f /data/crypto/tls.crt) && nc -z localhost ${GRPC_PORT:-50051} || exit 1"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Healthcheck ignores CRYPTO_TLS_CERT_PATH=none and custom cert paths.

This check hardcodes /data/crypto/tls.crt, so disabling TLS with CRYPTO_TLS_CERT_PATH=none or pointing at another cert path leaves the container permanently unhealthy even though packages/crypto/main.go supports both modes. Use the configured path when one is set, and treat none as a valid no-cert case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose.yml` at line 29, The healthcheck currently hardcodes
/data/crypto/tls.crt and fails when CRYPTO_TLS_CERT_PATH is set to "none" or a
custom path; update the healthcheck command in docker-compose.yml (the test line
that references CRYPTO_TLS_CERT_PATH and GRPC_PORT) to: 1) treat
CRYPTO_TLS_CERT_PATH="none" as valid (skip file existence check), 2) if
CRYPTO_TLS_CERT_PATH is non-empty and not "none" check that that file exists,
and 3) if CRYPTO_TLS_CERT_PATH is empty fall back to the default
/data/crypto/tls.crt; keep the existing port check (nc -z localhost
${GRPC_PORT:-50051}) and exit nonzero on failure.

Comment thread packages/crypto/main.go
Comment thread packages/crypto/main.go Outdated
Comment on lines +58 to +69
strategy="lazyOnload"
onLoad={() => {
// @ts-expect-error -- posthog is loaded via external script
window.posthog?.init(posthogKey, {
api_host: posthogHost,
capture_pageview: true,
capture_pageleave: true,
autocapture: true,
persistence: 'localStorage+cookie',
capture_performance: true
})`}
</Script>
</>
capture_performance: true,
})
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify mount-time analytics calls that can fire before PostHog is initialized.
rg -nP --type=tsx -C3 'useEffect\s*\(\s*\(\)\s*=>[\s\S]{0,350}\btrackEvent\s*\(' packages/dashboard/app packages/dashboard/lib

Repository: keywaysh/keyway

Length of output: 86


🏁 Script executed:

# Search for useEffect hooks that call trackEvent
rg -n 'useEffect\s*\(' packages/dashboard/app packages/dashboard/lib -A 10 --type=ts | head -100

Repository: keywaysh/keyway

Length of output: 7614


🏁 Script executed:

# Look at the specific files mentioned in the review
fd -t f 'login/page\.(tsx|ts)' packages/dashboard/app
fd -t f 'auth/callback/page\.(tsx|ts)' packages/dashboard/app

Repository: keywaysh/keyway

Length of output: 41


🏁 Script executed:

# Check the layout.tsx file for the Script block
cat packages/dashboard/app/layout.tsx

Repository: keywaysh/keyway

Length of output: 2206


🏁 Script executed:

fd -t f 'login/page\.(tsx|ts)' packages/dashboard/app -exec cat {} \;

Repository: keywaysh/keyway

Length of output: 205


🏁 Script executed:

cat packages/dashboard/lib/analytics.ts

Repository: keywaysh/keyway

Length of output: 4096


🏁 Script executed:

find packages/dashboard/app -name 'page.tsx' -o -name 'page.ts' | grep -i login

Repository: keywaysh/keyway

Length of output: 96


🏁 Script executed:

find packages/dashboard/app -name 'page.tsx' -o -name 'page.ts' | head -20

Repository: keywaysh/keyway

Length of output: 1033


🏁 Script executed:

cat packages/dashboard/app/login/page.tsx

Repository: keywaysh/keyway

Length of output: 4883


Race condition between lazyOnload and mount-time event tracking

The lazyOnload strategy defers PostHog initialization until after browser idle, but trackEvent() is called on mount in login/page.tsx (line 28) and auth/callback/page.tsx (lines 16, 22). Since trackEvent silently no-ops when window.posthog is undefined, early analytics events can be lost.

Consider switching to strategy="afterInteractive" or implementing an event queue to capture events before script load.

Additionally, the @ts-expect-error comment is unnecessary—window.posthog is already properly typed as optional in analytics.ts, so the type is known to TypeScript.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dashboard/app/layout.tsx` around lines 58 - 69, The PostHog init in
layout.tsx uses strategy="lazyOnload" causing a race where trackEvent calls in
login/page.tsx and auth/callback/page.tsx can be lost; change the script loading
strategy to "afterInteractive" so window.posthog is available on mount, or
implement an event queue (e.g., push events to a temporary array and flush them
in the init callback inside the onLoad closure) to capture early events; also
remove the unnecessary // `@ts-expect-error` before window.posthog since
analytics.ts already types window.posthog as optional.

- Default CRYPTO_TLS_CERT_PATH=none in docker-compose so plain
  `docker compose up` stays in insecure mode (no TLS mismatch)
- Validate cert expiration on load, auto-regenerate if expired,
  warn if expiring within 30 days
- Atomic cert/key writes via temp files + rename to prevent
  half-written pairs on crash
- Simplify healthcheck back to nc -z (cert check unnecessary
  since default is now TLS off)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/backend/src/utils/remoteEncryption.ts (1)

189-198: Consider wrapping file read with a descriptive error message.

If tlsCaPath points to a non-existent or unreadable file, fs.readFileSync throws a generic Node.js error (ENOENT/EACCES). A contextual wrapper would help operators diagnose misconfiguration faster.

♻️ Suggested improvement
   if (options?.tlsCaPath) {
     // Path was explicitly configured -- fail loudly if unreadable
-    return fs.readFileSync(options.tlsCaPath);
+    try {
+      return fs.readFileSync(options.tlsCaPath);
+    } catch (err) {
+      const message = err instanceof Error ? err.message : String(err);
+      throw new Error(
+        `Failed to read TLS CA certificate from "${options.tlsCaPath}": ${message}`
+      );
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/utils/remoteEncryption.ts` around lines 189 - 198, The
file-read in loadTlsCa (class using CryptoServiceOptions) should be wrapped in a
try/catch so unreadable/missing tlsCaPath produces a descriptive error; catch
errors from fs.readFileSync(options.tlsCaPath) and rethrow a new Error that
includes the tlsCaPath value and the original error message (or attach original
error) to give clear context for operators when loadTlsCa fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/backend/src/utils/remoteEncryption.ts`:
- Around line 189-198: The file-read in loadTlsCa (class using
CryptoServiceOptions) should be wrapped in a try/catch so unreadable/missing
tlsCaPath produces a descriptive error; catch errors from
fs.readFileSync(options.tlsCaPath) and rethrow a new Error that includes the
tlsCaPath value and the original error message (or attach original error) to
give clear context for operators when loadTlsCa fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 188921bc-bc46-4e64-ad96-691ae71b098e

📥 Commits

Reviewing files that changed from the base of the PR and between 9d25ef0 and e87e408.

📒 Files selected for processing (1)
  • packages/backend/src/utils/remoteEncryption.ts

- .env.example: deployment-specific guidance (Docker Compose uses shared
  volume, Railway only needs auth token, Kubernetes via Secret)
- README.md: updated security section with new env vars and
  per-platform recommendations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/crypto/README.md (1)

187-187: Soften “TLS unnecessary” wording to avoid weakening security posture.

At Line 187, saying TLS is “unnecessary” on Railway is too absolute. Recommend framing it as optional in trusted private networking, while still preferring TLS for defense-in-depth.

✏️ Suggested doc tweak
-6. **TLS**: Set `CRYPTO_TLS_CERT_PATH` to enable auto-generated self-signed TLS. The cert is shared with the backend via Docker volume or env var. Recommended for Docker Compose and Kubernetes. On Railway, the private network isolation makes TLS unnecessary -- use `CRYPTO_AUTH_TOKEN` alone.
+6. **TLS**: Set `CRYPTO_TLS_CERT_PATH` to enable auto-generated self-signed TLS. The cert is shared with the backend via Docker volume or env var. Recommended for Docker Compose and Kubernetes. On Railway private networking, TLS can be optional if you enforce `CRYPTO_AUTH_TOKEN`, but TLS is still preferred when feasible for defense-in-depth.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/crypto/README.md` at line 187, Update the README sentence that
currently states TLS is "unnecessary" on Railway (referencing
CRYPTO_TLS_CERT_PATH and CRYPTO_AUTH_TOKEN) to a softer recommendation: say TLS
can be optional in trusted private networking environments like Railway but is
still recommended for defense-in-depth; keep mention that CRYPTO_TLS_CERT_PATH
enables auto-generated self-signed TLS and that CRYPTO_AUTH_TOKEN can be used
alone when private network isolation is trusted. Ensure the revised line
preserves guidance about sharing the cert with the backend via Docker volume or
env var and recommends preferring TLS when possible.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/crypto/README.md`:
- Line 187: Update the README sentence that currently states TLS is
"unnecessary" on Railway (referencing CRYPTO_TLS_CERT_PATH and
CRYPTO_AUTH_TOKEN) to a softer recommendation: say TLS can be optional in
trusted private networking environments like Railway but is still recommended
for defense-in-depth; keep mention that CRYPTO_TLS_CERT_PATH enables
auto-generated self-signed TLS and that CRYPTO_AUTH_TOKEN can be used alone when
private network isolation is trusted. Ensure the revised line preserves guidance
about sharing the cert with the backend via Docker volume or env var and
recommends preferring TLS when possible.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 729c84da-78c2-41de-93cb-19d548ecfa52

📥 Commits

Reviewing files that changed from the base of the PR and between e87e408 and d4a46c1.

📒 Files selected for processing (4)
  • docker-compose.yml
  • packages/backend/.env.example
  • packages/crypto/README.md
  • packages/crypto/main.go
✅ Files skipped from review due to trivial changes (1)
  • packages/backend/.env.example
🚧 Files skipped from review as they are similar to previous changes (2)
  • docker-compose.yml
  • packages/crypto/main.go

@NicolasRitouet
NicolasRitouet merged commit 77fe93a into main Apr 9, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant