-
Notifications
You must be signed in to change notification settings - Fork 11
fix(web): secure-context-safe UUIDs + HTTPS compose overlay for LAN deployments #864
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| # HTTPS overlay for LAN deployments (e.g. a NAS reached by IP or intranet name). | ||
| # Browsers expose crypto.randomUUID, crypto.subtle, and navigator.clipboard only | ||
| # in secure contexts (HTTPS or localhost), so a console served over plain HTTP | ||
| # from a non-localhost host degrades. This overlay puts Caddy in front of web, | ||
| # control-plane, and relay with certificates from Caddy's internal CA: | ||
| # | ||
| # AGENTCONNECT_HTTPS_HOST=<LAN IP or hostname> \ | ||
| # docker compose -f compose.yaml -f compose.https.yaml up -d | ||
| # | ||
| # Then open https://<host>:3443. Trust the generated CA once per device | ||
| # (recommended; otherwise visit each of the three HTTPS origins once and accept | ||
| # the certificate warning): | ||
| # | ||
| # docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt agentconnect-root-ca.crt | ||
| # | ||
| # Daemons are unaffected by secure contexts and keep dialing the plain HTTP/WS | ||
| # ports, so AGENTCONNECT_BIND_ADDRESS / AGENTCONNECT_RELAY_DAEMON_URL work as in | ||
| # the base file. Do not expose a no-auth stack beyond your trusted network. | ||
|
|
||
| services: | ||
| caddy: | ||
| image: caddy:2-alpine | ||
| depends_on: | ||
| web: | ||
| condition: service_healthy | ||
| control-plane: | ||
| condition: service_healthy | ||
| relay: | ||
| condition: service_healthy | ||
| environment: | ||
| AGENTCONNECT_HTTPS_HOST: ${AGENTCONNECT_HTTPS_HOST:?set AGENTCONNECT_HTTPS_HOST to the LAN hostname or IP browsers will use} | ||
| ports: | ||
| - ${AGENTCONNECT_TLS_BIND_ADDRESS:-0.0.0.0}:${AGENTCONNECT_WEB_TLS_PORT:-3443}:3443 | ||
| - ${AGENTCONNECT_TLS_BIND_ADDRESS:-0.0.0.0}:${AGENTCONNECT_CP_TLS_PORT:-8443}:8443 | ||
| - ${AGENTCONNECT_TLS_BIND_ADDRESS:-0.0.0.0}:${AGENTCONNECT_RELAY_TLS_PORT:-9443}:9443 | ||
| restart: unless-stopped | ||
| volumes: | ||
| - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro | ||
| # Persists the internal CA so trusted roots survive container recreation. | ||
| - caddy-data:/data | ||
| - caddy-config:/config | ||
|
|
||
| setup-server: | ||
| environment: | ||
| - AGENTCONNECT_PUBLIC_CP_URL=https://${AGENTCONNECT_HTTPS_HOST}:${AGENTCONNECT_CP_TLS_PORT:-8443} | ||
| - AGENTCONNECT_PUBLIC_WEB_URL=https://${AGENTCONNECT_HTTPS_HOST}:${AGENTCONNECT_WEB_TLS_PORT:-3443} | ||
| - AGENTCONNECT_PUBLIC_RELAY_URL=https://${AGENTCONNECT_HTTPS_HOST}:${AGENTCONNECT_RELAY_TLS_PORT:-9443} | ||
|
|
||
| control-plane: | ||
| environment: | ||
| - PUBLIC_CP_URL=https://${AGENTCONNECT_HTTPS_HOST}:${AGENTCONNECT_CP_TLS_PORT:-8443} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Keep daemon onboarding usable with the internal CA. |
||
| - PUBLIC_WEB_URL=https://${AGENTCONNECT_HTTPS_HOST}:${AGENTCONNECT_WEB_TLS_PORT:-3443} | ||
| - PUBLIC_RELAY_URL=https://${AGENTCONNECT_HTTPS_HOST}:${AGENTCONNECT_RELAY_TLS_PORT:-9443} | ||
| - CORS_ORIGIN=https://${AGENTCONNECT_HTTPS_HOST}:${AGENTCONNECT_WEB_TLS_PORT:-3443} | ||
|
|
||
| web: | ||
| environment: | ||
| - PUBLIC_WEB_URL=https://${AGENTCONNECT_HTTPS_HOST}:${AGENTCONNECT_WEB_TLS_PORT:-3443} | ||
| - CP_URL=https://${AGENTCONNECT_HTTPS_HOST}:${AGENTCONNECT_CP_TLS_PORT:-8443}/api/v1 | ||
| - RELAY_URL=https://${AGENTCONNECT_HTTPS_HOST}:${AGENTCONNECT_RELAY_TLS_PORT:-9443} | ||
|
|
||
| volumes: | ||
| caddy-data: | ||
| caddy-config: | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # TLS termination for LAN deployments (compose.https.yaml). One HTTPS port per | ||
| # service so no route knowledge leaks into the proxy; certificates come from | ||
| # Caddy's internal CA (trust root.crt once per device — see compose.https.yaml). | ||
| { | ||
| local_certs | ||
| admin off | ||
| auto_https disable_redirects | ||
| } | ||
|
|
||
| https://{$AGENTCONNECT_HTTPS_HOST}:3443 { | ||
| reverse_proxy web:8080 | ||
| } | ||
|
|
||
| https://{$AGENTCONNECT_HTTPS_HOST}:8443 { | ||
| reverse_proxy control-plane:8080 | ||
| } | ||
|
|
||
| https://{$AGENTCONNECT_HTTPS_HOST}:9443 { | ||
| reverse_proxy relay:8080 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { afterEach, describe, expect, it, vi } from 'vitest' | ||
| import { randomUUID } from './random-id' | ||
|
|
||
| const V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ | ||
|
|
||
| describe('randomUUID', () => { | ||
| afterEach(() => vi.unstubAllGlobals()) | ||
|
|
||
| it('uses the native implementation when available', () => { | ||
| const native = vi.fn(() => '11111111-2222-4333-8444-555555555555') | ||
| vi.stubGlobal('crypto', { randomUUID: native }) | ||
| expect(randomUUID()).toBe('11111111-2222-4333-8444-555555555555') | ||
| expect(native).toHaveBeenCalledOnce() | ||
| }) | ||
|
|
||
| it('builds a valid v4 UUID over HTTP, where only getRandomValues exists', () => { | ||
| // Insecure contexts (HTTP on a LAN host) hide crypto.randomUUID but keep | ||
| // getRandomValues — the frames' z.string().uuid() must still accept the id. | ||
| const real = globalThis.crypto | ||
| vi.stubGlobal('crypto', { getRandomValues: real.getRandomValues.bind(real) }) | ||
| const seen = new Set(Array.from({ length: 64 }, () => randomUUID())) | ||
| for (const id of seen) expect(id).toMatch(V4) | ||
| expect(seen.size).toBe(64) | ||
| }) | ||
|
|
||
| it('still emits v4-shaped ids with no crypto object at all', () => { | ||
| vi.stubGlobal('crypto', undefined) | ||
| expect(randomUUID()).toMatch(V4) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // Browsers expose crypto.randomUUID only in secure contexts (HTTPS/localhost) — | ||
| // an HTTP console on a LAN host needs this manual v4 fallback (wire frames | ||
| // validate ids with z.string().uuid(), so the fallback must be a real UUID). | ||
| export function randomUUID(): string { | ||
| const c = globalThis.crypto | ||
| if (c?.randomUUID) return c.randomUUID() | ||
| const bytes = new Uint8Array(16) | ||
| // getRandomValues is NOT gated on secure contexts; Math.random is a last resort. | ||
| if (c?.getRandomValues) c.getRandomValues(bytes) | ||
| else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256) | ||
| bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40 // version 4 | ||
| bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80 // RFC 4122 variant | ||
| const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('') | ||
| return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Carry the required host into the certificate-export command. The preceding
AGENTCONNECT_HTTPS_HOST=...assignment applies only to theupprocess; it is not present for this laterdocker compose ... cp. Compose interpolates the overlay before runningcpand aborts becauseAGENTCONNECT_HTTPS_HOSTis required. Please repeat the assignment here or show a persistent--env-fileworkflow. The analogous command incompose.https.yamlalso needs the overlay file set (and the host value).