From 1e950b1295ab873c8c9d03d727a05cb9e473d024 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 14 Jun 2026 09:13:17 +0000 Subject: [PATCH] fix(ci-setup): harden generated deployment security defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - No-auth docker-compose now publishes via ${GITNEXUS_HOST:-127.0.0.1} so it binds loopback by default (not 0.0.0.0) and is not exposed by accident; operators set GITNEXUS_HOST to an interface IP / 0.0.0.0 for LAN access. The container healthcheck keeps localhost (it curls its own loopback, so it must not use GITNEXUS_HOST). - Caddyfile gains TLS guidance (the proxy is plain HTTP, so the bearer token is cleartext — use a hostname for Caddy auto-HTTPS or terminate TLS upstream) and a standalone empty-token warning (compose already enforces a non-empty GITNEXUS_TOKEN via ${GITNEXUS_TOKEN:?...}). - GITNEXUS.md (token mode) warns the bearer token travels in cleartext and to put the server behind TLS before exposing it. Co-Authored-By: Claude Opus 4.8 (1M context) --- gitnexus/src/cli/ci-setup/templates.ts | 26 ++++++++++++++++--- gitnexus/test/unit/ci-setup-templates.test.ts | 26 +++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/gitnexus/src/cli/ci-setup/templates.ts b/gitnexus/src/cli/ci-setup/templates.ts index c6a32c1dc..ea2f0aad0 100644 --- a/gitnexus/src/cli/ci-setup/templates.ts +++ b/gitnexus/src/cli/ci-setup/templates.ts @@ -186,12 +186,16 @@ ${GENERATED_NOTICE} # ⚠ NO AUTH — trusted internal network only. # Anyone who can reach port ${opts.port} has full access to all indexed repositories. # Do not expose this port to the public internet. +# +# The publish address is controlled by GITNEXUS_HOST (default: 127.0.0.1, loopback +# only — so it is not exposed by accident). Set GITNEXUS_HOST to an interface IP +# (or 0.0.0.0) to allow LAN access, only on a trusted network. services: gitnexus: image: ${GITNEXUS_IMAGE} ports: - - "\${GITNEXUS_PORT:-${opts.port}}:${CONTAINER_PORT}" + - "\${GITNEXUS_HOST:-127.0.0.1}:\${GITNEXUS_PORT:-${opts.port}}:${CONTAINER_PORT}" volumes: - gitnexus-data:/data/gitnexus - \${WORKSPACE_DIR:-./workspace}:/workspace:ro @@ -216,7 +220,19 @@ function buildDockerCompose(opts: CiSetupOptions): string { function buildCaddyfile(opts: CiSetupOptions): string { const proxyPort = caddyProxyPort(opts); - return `:${proxyPort} { + return `# Generated by: gitnexus ci-setup +# +# ⚠ This proxy listens on plain HTTP, so the GITNEXUS_TOKEN bearer token travels +# in cleartext. Do NOT expose it beyond a trusted host/LAN as-is. For an +# internet-reachable deployment, give the site a real hostname so Caddy +# auto-provisions HTTPS (replace ":${proxyPort}" with "your.host.example" and let +# Caddy serve :443), or terminate TLS at an upstream load balancer. +# +# GITNEXUS_TOKEN MUST be set to a non-empty value. The bundled docker-compose +# enforces this (\${GITNEXUS_TOKEN:?...}); if you run this Caddyfile standalone, +# set the env var first — otherwise the matcher below becomes "Bearer " and any +# request with an empty bearer token is accepted. +:${proxyPort} { @authorized header Authorization "Bearer {env.GITNEXUS_TOKEN}" handle @authorized { @@ -394,7 +410,11 @@ Add to \`~/.cursor/mcp.json\` under \`mcpServers\`: } \`\`\` -*Or* point Cursor at the shared HTTP server using the same \`type: http\` entry as Claude Code.` +*Or* point Cursor at the shared HTTP server using the same \`type: http\` entry as Claude Code. + +> **Security:** the proxy serves plain HTTP, so the bearer token is sent in cleartext. Put it behind +> TLS — give Caddy a real hostname for automatic HTTPS, or terminate TLS at a load balancer — before +> exposing the server beyond localhost or a trusted LAN.` : `### Claude Code Copy the MCP entry from \`.claude/gitnexus-mcp-snippet.json\` into \`~/.claude/settings.json\`: diff --git a/gitnexus/test/unit/ci-setup-templates.test.ts b/gitnexus/test/unit/ci-setup-templates.test.ts index d4febfe02..d5c4f3df6 100644 --- a/gitnexus/test/unit/ci-setup-templates.test.ts +++ b/gitnexus/test/unit/ci-setup-templates.test.ts @@ -86,6 +86,32 @@ describe('GITNEXUS.md / workflow accuracy (U8)', () => { }); }); +describe('security hardening (U5)', () => { + it('no-auth compose binds via GITNEXUS_HOST defaulting to loopback', () => { + const files = generateFiles(makeOpts({ auth: 'none', port: 4747 }), DEFAULT_DETECT); + const dc = files.find((f) => f.relativePath === 'docker-compose.gitnexus.yml'); + expect(dc?.content).toContain('${GITNEXUS_HOST:-127.0.0.1}:${GITNEXUS_PORT:-4747}:4747'); + // still valid YAML with the host prefix + expect(() => yaml.load(dc?.content ?? '')).not.toThrow(); + }); + + it('Caddyfile carries TLS guidance and a standalone empty-token warning', () => { + const files = generateFiles(makeOpts({ auth: 'token' }), DEFAULT_DETECT); + const cf = files.find((f) => f.relativePath === 'Caddyfile'); + expect(cf?.content).toContain('HTTPS'); + expect(cf?.content).toContain('cleartext'); + expect(cf?.content).toContain('GITNEXUS_TOKEN MUST be set'); + // the gate itself is unchanged (fail-closed for a non-empty token) + expect(cf?.content).toContain('respond "Unauthorized" 401'); + }); + + it('GITNEXUS.md (token mode) warns the bearer token is cleartext over HTTP', () => { + const content = gitnexusMd({ auth: 'token' }); + expect(content).toContain('cleartext'); + expect(content).toContain('TLS'); + }); +}); + describe('version pinning (U9)', () => { it('pins the GitHub Actions workflow analyze step to the wizard version', () => { const files = generateFiles(makeOpts({ version: '9.9.9' }), DEFAULT_DETECT);