Skip to content

Commit a5e6b04

Browse files
fix(docker): wire required prod env + fix webhook path for auth/integration cutover (#1325)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e6468a8 commit a5e6b04

7 files changed

Lines changed: 60 additions & 73 deletions

File tree

docker/.env.example

Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@ APP_HOSTNAME=
1919
# Docker image tag to deploy (e.g., latest, v1.0.0, develop)
2020
IMAGE_TAG=latest
2121

22-
# Timezone for containers
23-
TIMEZONE=UTC
22+
# Tomcat RemoteIpValve internal-proxies REGEX (NOT CIDR) matching ONLY the ingress (Traefik) address.
23+
# REQUIRED in prod: forward-headers-strategy=native is active and ProxyTrustGuard fails the boot if
24+
# blank (Boot's default trusts all of RFC-1918, letting a client spoof X-Forwarded-For and defeat the
25+
# pre-auth IP rate limit). Find Traefik's address with `docker network inspect shared-network` and
26+
# match its /24, e.g. 172\.18\.0\.\d+. The bridge subnet is assigned dynamically unless pinned, so
27+
# re-verify after recreation (or pin a subnet / static Traefik IP to keep this value stable).
28+
HEPHAESTUS_TRUSTED_PROXIES=
2429

2530
# -----------------------------------------------------------------------------
2631
# AUTHENTICATION (Hephaestus-native — no Keycloak; ADR 0017)
@@ -78,20 +83,6 @@ WEBHOOK_SECRET=
7883
# requires re-encrypting every connection row. Generate: `openssl rand -base64 24 | cut -c1-32`
7984
HEPHAESTUS_SECURITY_ENCRYPTION_KEY=
8085

81-
# -----------------------------------------------------------------------------
82-
# AI MODELS
83-
# -----------------------------------------------------------------------------
84-
85-
# Model names in format provider:model (e.g., openai:gpt-4o, azure:gpt-5.4-mini)
86-
MODEL_NAME=openai:gpt-4o
87-
88-
# OpenAI API
89-
OPENAI_API_KEY=
90-
91-
# Azure OpenAI (alternative to OpenAI)
92-
AZURE_RESOURCE_NAME=
93-
AZURE_API_KEY=
94-
9586
# -----------------------------------------------------------------------------
9687
# NATS - Message Queue
9788
# -----------------------------------------------------------------------------
@@ -125,21 +116,12 @@ MONITORING_BACKFILL_INTERVAL_SECONDS=60
125116
# LEADERBOARD NOTIFICATIONS
126117
# -----------------------------------------------------------------------------
127118

128-
# Enable Slack notifications for leaderboard
119+
# Global kill-switch for the Slack leaderboard digest. The target channel/team are
120+
# configured per-workspace on the Slack Connection, not here.
129121
LEADERBOARD_NOTIFICATION_ENABLED=false
130122

131-
# Team name to show in notifications
132-
LEADERBOARD_NOTIFICATION_TEAM=
133-
134-
# Slack channel ID for notifications
135-
LEADERBOARD_NOTIFICATION_CHANNEL_ID=
136-
137-
# Comma-separated workspace slugs allowed to receive notifications.
138-
# Temporary allowlist until per-workspace Slack routing ships.
139-
LEADERBOARD_NOTIFICATION_WORKSPACE_SLUGS=
140-
141-
# Schedule for leaderboard notifications
142-
LEADERBOARD_SCHEDULE_DAY=MONDAY
123+
# Schedule: day is an integer 1-7 (ISO-8601, Mon=1 … Sun=7); time is HH[:MM].
124+
LEADERBOARD_SCHEDULE_DAY=2
143125
LEADERBOARD_SCHEDULE_TIME=09:00
144126

145127
# -----------------------------------------------------------------------------
@@ -209,11 +191,9 @@ GITLAB_WORKSPACE_CREATION=false
209191

210192
# Agent image override (dev only): see docs/admin/agent-image-digests.md.
211193

212-
# Enable the Docker sandbox for running agent containers
213-
SANDBOX_ENABLED=false
214-
215-
# Practice review requires SANDBOX_ENABLED, AGENT_NATS_ENABLED,
216-
# GIT_CHECKOUT_ENABLED, and NATS_ENABLED to be enabled together.
194+
# The sandbox is activated by the worker role (hephaestus.runtime.worker.enabled, default true
195+
# in the monolith), not a toggle. Practice review additionally needs AGENT_NATS_ENABLED,
196+
# GIT_CHECKOUT_ENABLED, and NATS_ENABLED enabled together.
217197

218198
# Docker daemon endpoint (default: local socket)
219199
# SANDBOX_DOCKER_HOST=unix:///var/run/docker.sock
@@ -268,10 +248,9 @@ PRACTICE_REVIEW_FOR_ALL=false
268248
# Must be ≥32 chars. Rotate by setting a new value and restarting both pods.
269249
# HEPHAESTUS_WORKER_REGISTRATION_TOKEN=
270250

271-
# RSA-2048 private key (PEM) the hub uses to sign worker JWTs. Generate with:
272-
# openssl genrsa -out worker-signing.pem 2048
273-
# The hub derives the matching public key from CRT params.
274-
# HEPHAESTUS_WORKER_HUB_SIGNING_KEY=
251+
# Note: the hub's JWT signing key is a keyring (hephaestus.worker.hub.token.keys[*].private-key +
252+
# active-kid), not a single env var. With none configured the hub uses an ephemeral key regenerated
253+
# on restart (fine for a single replica). A stable multi-replica key has no compose passthrough yet.
275254

276255
# LLM endpoint and key the worker passes to sandboxes. Same values you set on
277256
# the app-server LLM_PROXY_OPENAI_URL but consumed in-process here.

docker/compose.app.yaml

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ services:
9797
APP_VERSION: ${IMAGE_TAG}
9898
SPRING_PROFILES_ACTIVE: prod
9999
APPLICATION_HOST_URL: https://${APP_HOSTNAME}
100+
# Tomcat RemoteIpValve proxy trust. forward-headers-strategy=native (application-prod.yml) is
101+
# active in prod, so ProxyTrustGuard fails the boot unless this is a non-blank internal-proxies
102+
# regex pinned to the ingress (Traefik). Empty default surfaces the guard's clear fail-fast.
103+
HEPHAESTUS_TRUSTED_PROXIES: ${HEPHAESTUS_TRUSTED_PROXIES:-}
100104
# Webhook reception runs in a dedicated `webhook-server` container (see ADR 0008
101105
# + docker/compose.core.yaml). Disable it here so the receiver beans don't load
102106
# in application-server (saves RAM; prevents accidental localhost traffic).
@@ -108,40 +112,45 @@ services:
108112
# Auth (Hephaestus-native; ADR 0017). State-cookie key MUST be base64 32-byte AES.
109113
HEPHAESTUS_AUTH_ISSUER: https://${APP_HOSTNAME}
110114
HEPHAESTUS_AUTH_STATE_COOKIE_KEY: ${HEPHAESTUS_AUTH_STATE_COOKIE_KEY}
115+
# First instance super-admin bootstrap (auth-cutover runbook). The admin UI is admin-gated, so
116+
# the first APP_ADMIN must come from operator config — comma-separated <provider>:@user or
117+
# <provider>:<subject>. Optional break-glass token enables POST /auth/bootstrap-admin.
118+
HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS: ${HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS:-}
119+
HEPHAESTUS_AUTH_BOOTSTRAP_TOKEN: ${HEPHAESTUS_AUTH_BOOTSTRAP_TOKEN:-}
111120
GITHUB_OAUTH_CLIENT_ID: ${GITHUB_OAUTH_CLIENT_ID}
112121
GITHUB_OAUTH_CLIENT_SECRET: ${GITHUB_OAUTH_CLIENT_SECRET}
113122
GITLAB_OAUTH_CLIENT_ID: ${GITLAB_OAUTH_CLIENT_ID:-}
114123
GITLAB_OAUTH_CLIENT_SECRET: ${GITLAB_OAUTH_CLIENT_SECRET:-}
115124
GITLAB_OAUTH_BASE_URL: ${GITLAB_OAUTH_BASE_URL:-https://gitlab.com}
116125
GITLAB_OAUTH_DISPLAY_NAME: ${GITLAB_OAUTH_DISPLAY_NAME:-GitLab}
117-
GH_APP_ID: ${GH_APP_ID}
126+
GH_APP_ID: ${GH_APP_ID:-0}
118127
GH_APP_PRIVATE_KEY: ${GH_APP_PRIVATE_KEY}
119128
GH_APP_PRIVATE_KEY_LOCATION: ${GH_APP_PRIVATE_KEY_LOCATION:-}
120129
GH_APP_INSTALLATION_URL: ${GH_APP_INSTALLATION_URL:-}
121130
GH_AUTH_TOKEN: ${GH_AUTH_TOKEN}
122-
NATS_ENABLED: ${NATS_ENABLED}
123-
NATS_DURABLE_CONSUMER_NAME: ${NATS_DURABLE_CONSUMER_NAME}
124-
MONITORING_TIMEFRAME: ${MONITORING_TIMEFRAME}
125-
MONITORING_RUN_ON_STARTUP: ${MONITORING_RUN_ON_STARTUP}
126-
MONITORING_SYNC_CRON: ${MONITORING_SYNC_CRON}
127-
MONITORING_SYNC_COOLDOWN_IN_MINUTES: ${MONITORING_SYNC_COOLDOWN_IN_MINUTES}
131+
# Default on: the integration event flow needs NATS in prod (the code-level fallback is off for local dev).
132+
NATS_ENABLED: ${NATS_ENABLED:-true}
133+
NATS_DURABLE_CONSUMER_NAME: ${NATS_DURABLE_CONSUMER_NAME:-}
134+
MONITORING_TIMEFRAME: ${MONITORING_TIMEFRAME:-7}
135+
MONITORING_RUN_ON_STARTUP: ${MONITORING_RUN_ON_STARTUP:-true}
136+
MONITORING_SYNC_CRON: ${MONITORING_SYNC_CRON:-0 0 * * * *}
137+
MONITORING_SYNC_COOLDOWN_IN_MINUTES: ${MONITORING_SYNC_COOLDOWN_IN_MINUTES:-60}
128138
MONITORING_BACKFILL_ENABLED: ${MONITORING_BACKFILL_ENABLED:-true}
129139
MONITORING_BACKFILL_BATCH_SIZE: ${MONITORING_BACKFILL_BATCH_SIZE:-50}
130140
MONITORING_BACKFILL_RATE_LIMIT_THRESHOLD: ${MONITORING_BACKFILL_RATE_LIMIT_THRESHOLD:-500}
131141
MONITORING_BACKFILL_INTERVAL_SECONDS: ${MONITORING_BACKFILL_INTERVAL_SECONDS:-60}
132-
LEADERBOARD_NOTIFICATION_ENABLED: ${LEADERBOARD_NOTIFICATION_ENABLED}
133-
LEADERBOARD_NOTIFICATION_TEAM: ${LEADERBOARD_NOTIFICATION_TEAM}
134-
LEADERBOARD_NOTIFICATION_CHANNEL_ID: ${LEADERBOARD_NOTIFICATION_CHANNEL_ID}
135-
LEADERBOARD_NOTIFICATION_WORKSPACE_SLUGS: ${LEADERBOARD_NOTIFICATION_WORKSPACE_SLUGS}
136-
LEADERBOARD_SCHEDULE_DAY: ${LEADERBOARD_SCHEDULE_DAY}
137-
LEADERBOARD_SCHEDULE_TIME: ${LEADERBOARD_SCHEDULE_TIME}
142+
# Per-workspace channel/team now live on the Slack Connection config; this is the global kill-switch.
143+
LEADERBOARD_NOTIFICATION_ENABLED: ${LEADERBOARD_NOTIFICATION_ENABLED:-true}
144+
LEADERBOARD_SCHEDULE_DAY: ${LEADERBOARD_SCHEDULE_DAY:-2}
145+
LEADERBOARD_SCHEDULE_TIME: ${LEADERBOARD_SCHEDULE_TIME:-9}
138146
SENTRY_DSN: ${SENTRY_DSN}
139147
POSTHOG_ENABLED: ${POSTHOG_ENABLED:-false}
140148
POSTHOG_API_HOST: ${POSTHOG_API_HOST:-https://app.posthog.com}
141149
POSTHOG_PROJECT_ID: ${POSTHOG_PROJECT_ID:-}
142150
POSTHOG_PERSONAL_API_KEY: ${POSTHOG_PERSONAL_API_KEY:-}
143151
WEBHOOK_SECRET: ${WEBHOOK_SECRET}
144-
WEBHOOK_EXTERNAL_URL: https://${APP_HOSTNAME}/webhooks
152+
# WEBHOOK_EXTERNAL_URL intentionally unset: GitLab auto-registration defaults it to
153+
# APPLICATION_HOST_URL (same origin here), then appends /webhooks/gitlab itself.
145154
# AES-256-GCM key for credentials encrypted at rest in the `connection` table. Exactly 32
146155
# chars. Required in prod (fail-fast); the Liquibase backfill re-encrypts on boot, so this
147156
# MUST be set on application-server (where Liquibase runs). Keep it stable.
@@ -154,9 +163,8 @@ services:
154163
# GitLab integration
155164
GITLAB_ENABLED: ${GITLAB_ENABLED:-false}
156165
GITLAB_DEFAULT_SERVER_URL: ${GITLAB_DEFAULT_SERVER_URL:-https://gitlab.lrz.de}
157-
# Agent sandbox (practice review + Pi mentor chat — both gated per-workspace via
158-
# WorkspaceFeatures.{practicesEnabled,mentorEnabled})
159-
SANDBOX_ENABLED: ${SANDBOX_ENABLED:-false}
166+
# Agent sandbox (practice review + Pi mentor chat). Activated by the worker role
167+
# (hephaestus.runtime.worker.enabled), then gated per-workspace via WorkspaceFeatures.
160168
SANDBOX_DOCKER_HOST: ${SANDBOX_DOCKER_HOST:-unix:///var/run/docker.sock}
161169
SANDBOX_CONTAINER_RUNTIME: ${SANDBOX_CONTAINER_RUNTIME:-}
162170
SANDBOX_MAX_CONCURRENT: ${SANDBOX_MAX_CONCURRENT:-5}
@@ -186,8 +194,6 @@ services:
186194
LLM_PROXY_AZURE_OPENAI_USE_BEARER: ${LLM_PROXY_AZURE_OPENAI_USE_BEARER:-false}
187195
# Worker control channel: same env var feeds the worker pod's registration-token claim.
188196
HEPHAESTUS_WORKER_HUB_TOKEN_REGISTRATION_TOKEN: ${HEPHAESTUS_WORKER_REGISTRATION_TOKEN:-}
189-
# PEM PKCS#8 RSA private key. Unset = ephemeral keypair regenerated on restart (dev only).
190-
HEPHAESTUS_WORKER_HUB_TOKEN_SIGNING_KEY: ${HEPHAESTUS_WORKER_HUB_SIGNING_KEY:-}
191197
depends_on:
192198
postgres:
193199
condition: service_started
@@ -256,11 +262,11 @@ services:
256262
environment:
257263
APP_VERSION: ${IMAGE_TAG}
258264
SPRING_PROFILES_ACTIVE: prod,worker
265+
HEPHAESTUS_TRUSTED_PROXIES: ${HEPHAESTUS_TRUSTED_PROXIES:-}
259266
HEPHAESTUS_HUB_URL: ws://application-server:8080/api/workers/connect
260267
HEPHAESTUS_WORKER_REGISTRATION_TOKEN: ${HEPHAESTUS_WORKER_REGISTRATION_TOKEN:-}
261268
NATS_SERVER: nats://nats-server:4222
262269
AGENT_NATS_ENABLED: ${AGENT_NATS_ENABLED:-false}
263-
SANDBOX_ENABLED: ${SANDBOX_ENABLED:-false}
264270
SANDBOX_DOCKER_HOST: ${SANDBOX_DOCKER_HOST:-unix:///var/run/docker.sock}
265271
SANDBOX_CONTAINER_RUNTIME: ${SANDBOX_CONTAINER_RUNTIME:-}
266272
SANDBOX_MAX_CONCURRENT: ${SANDBOX_MAX_CONCURRENT:-5}

docker/compose.core.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ services:
3030
DATABASE_USERNAME: root
3131
DATABASE_PASSWORD: root
3232
SENTRY_DSN: ${SENTRY_DSN}
33+
# forward-headers=native (application-prod.yml) → ProxyTrustGuard fails the boot unless pinned.
34+
HEPHAESTUS_TRUSTED_PROXIES: ${HEPHAESTUS_TRUSTED_PROXIES:-}
3335
depends_on:
3436
nats-server:
3537
condition: service_healthy
@@ -42,13 +44,14 @@ services:
4244
labels:
4345
- "traefik.enable=true"
4446
- "traefik.http.middlewares.gzip.compress=true"
45-
- "traefik.http.middlewares.https-webhook-server-stripprefix.stripprefix.prefixes=/webhooks"
4647
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
4748
- "traefik.http.routers.http-webhook-server.entryPoints=http"
4849
- "traefik.http.routers.http-webhook-server.middlewares=redirect-to-https"
4950
- "traefik.http.routers.http-webhook-server.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/webhooks`)"
5051
- "traefik.http.routers.https-webhook-server.entryPoints=https"
51-
- "traefik.http.routers.https-webhook-server.middlewares=https-webhook-server-stripprefix,gzip"
52+
# No stripprefix: the receiver owns the full "/webhooks/{kind}" path (controller + payload-size
53+
# filter + security chain all bind to /webhooks/**). Stripping it would 404 every delivery.
54+
- "traefik.http.routers.https-webhook-server.middlewares=gzip"
5255
- "traefik.http.routers.https-webhook-server.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/webhooks`)"
5356
- "traefik.http.routers.https-webhook-server.tls.certresolver=letsencrypt"
5457
- "traefik.http.routers.https-webhook-server.tls=true"

docker/preview/.env.example

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,6 @@ GH_AUTH_TOKEN=
5353
# Install URL shown in the workspace creation wizard
5454
GH_APP_INSTALLATION_URL=
5555

56-
# AI Model configuration (provider:model format)
57-
MODEL_NAME=openai:gpt-4o
58-
59-
# OpenAI
60-
OPENAI_API_KEY=
61-
62-
# Azure OpenAI (alternative to OpenAI)
63-
AZURE_RESOURCE_NAME=
64-
AZURE_API_KEY=
65-
6656
# -----------------------------------------------------------------------------
6757
# OPTIONAL
6858
# -----------------------------------------------------------------------------

docker/preview/compose.app.yaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ services:
191191
restart: unless-stopped
192192
environment:
193193
SPRING_PROFILES_ACTIVE: prod
194+
# prod runs forward-headers=native, so ProxyTrustGuard aborts the boot unless this is set.
195+
# Coolify fronts previews with its own proxy — set its ingress regex, not staging's range.
196+
HEPHAESTUS_TRUSTED_PROXIES: ${HEPHAESTUS_TRUSTED_PROXIES:?Required}
194197
# URL - Coolify provides SERVICE_FQDN_WEBAPP
195198
APPLICATION_HOST_URL: https://${SERVICE_FQDN_WEBAPP}
196199
DATABASE_URL: postgresql://postgres:5432/hephaestus
@@ -225,9 +228,11 @@ services:
225228
LEADERBOARD_NOTIFICATION_ENABLED: "false"
226229
# Observability (optional)
227230
SENTRY_DSN: ${SENTRY_DSN:-}
228-
# Webhook auto-registration
231+
# Webhook auto-registration. Override (not the APPLICATION_HOST_URL default) because previews
232+
# route webhooks via PREVIEW_DOMAIN, not the webapp's SERVICE_FQDN_WEBAPP. Bare origin:
233+
# GitLabWebhookService appends /webhooks/gitlab itself.
229234
WEBHOOK_SECRET: ${WEBHOOK_SECRET:-}
230-
WEBHOOK_EXTERNAL_URL: https://${PREVIEW_DOMAIN:?Required}/webhooks
235+
WEBHOOK_EXTERNAL_URL: https://${PREVIEW_DOMAIN:?Required}
231236
depends_on:
232237
postgres:
233238
condition: service_healthy

docker/preview/compose.shared-infra.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ services:
7777
HEPHAESTUS_SYNC_NATS_ENABLED: "true"
7878
HEPHAESTUS_SYNC_NATS_SERVER: "nats://nats-server:4222"
7979
WEBHOOK_SECRET: ${WEBHOOK_SECRET:?Required}
80+
# prod runs forward-headers=native → ProxyTrustGuard aborts the boot unless set (Coolify ingress regex).
81+
HEPHAESTUS_TRUSTED_PROXIES: ${HEPHAESTUS_TRUSTED_PROXIES:?Required}
8082
depends_on:
8183
nats-server:
8284
condition: service_healthy

server/src/main/resources/application.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,10 @@ hephaestus:
449449
# publish/stream/http blocks for the inbound receiver.
450450
# `secret` is shared by design — same value registered with the provider and used
451451
# to verify incoming bodies. The receiver runs in the `webhook-server` container.
452+
# external-url defaults to the app's own origin (APPLICATION_HOST_URL); set WEBHOOK_EXTERNAL_URL
453+
# only when the receiver is fronted at a different origin than the app (e.g. preview).
452454
webhook:
453-
external-url: ${WEBHOOK_EXTERNAL_URL:}
455+
external-url: ${WEBHOOK_EXTERNAL_URL:${APPLICATION_HOST_URL:}}
454456
secret: ${WEBHOOK_SECRET:}
455457
token-rotation:
456458
threshold-days: ${WEBHOOK_TOKEN_ROTATION_THRESHOLD_DAYS:7}

0 commit comments

Comments
 (0)