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
8 changes: 8 additions & 0 deletions .changeset/self-host-broker-credentials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"hephaestus": patch
---

A fresh self-host install now starts instead of stopping with `required variable NATS_USERNAME is
missing a value`: `setup.sh` generates the message-broker credentials alongside the database password
and the other internal secrets. Upgrading an existing installation picks them up by rerunning
`docker/self-host/setup.sh`, which leaves every value you already set untouched.
12 changes: 4 additions & 8 deletions .github/workflows/ci-compose-validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,13 @@ jobs:
working-directory: docker/self-host
run: |
set -euo pipefail
# Supply required operator values; missing defaults must still fail rendering.
cp .env.example .env
# Render what the install guide produces: the installer's generated secrets plus the
# values step 2 asks the operator for. Hand-filling the generated ones here would hide a
# required variable the installer forgets, which is how one reached a release smoke test.
./setup.sh
sed -i \
-e 's|^APP_HOSTNAME=$|APP_HOSTNAME=hephaestus.example.com|' \
-e 's|^ACME_EMAIL=$|ACME_EMAIL=operator@example.com|' \
-e 's|^POSTGRES_PASSWORD=$|POSTGRES_PASSWORD=ci-not-a-real-password|' \
-e 's|^HEPHAESTUS_SECURITY_ENCRYPTION_KEY=$|HEPHAESTUS_SECURITY_ENCRYPTION_KEY=0123456789abcdef0123456789abcdef|' \
-e 's|^HEPHAESTUS_AUTH_STATE_COOKIE_KEY=$|HEPHAESTUS_AUTH_STATE_COOKIE_KEY=Y2ktbm90LWEtcmVhbC1zdGF0ZS1jb29raWUta2V5|' \
-e 's|^WEBHOOK_SECRET=$|WEBHOOK_SECRET=ci000000000000000000000000000000000|' \
-e 's|^NATS_USERNAME=$|NATS_USERNAME=ci|' \
-e 's|^NATS_PASSWORD=$|NATS_PASSWORD=ci-not-a-real-password|' \
.env
while IFS= read -r image; do
name="HEPHAESTUS_IMAGE_${image^^}"
Expand Down
3 changes: 2 additions & 1 deletion docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ HEPHAESTUS_SECURITY_CREDENTIAL_ENCRYPTION_KEY=
# -----------------------------------------------------------------------------

NATS_ENABLED=true
# Required. Generate independent random values; do not reuse an application key.
# Required. Generate independent random values; do not reuse an application key. Keep a letter in
# each: the broker's config file reads an all-digit credential as a number and refuses to start.
NATS_USERNAME=
NATS_PASSWORD=
NATS_DURABLE_CONSUMER_NAME=hephaestus-consumer
Expand Down
4 changes: 3 additions & 1 deletion docker/self-host/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ HEPHAESTUS_AUTH_STATE_COOKIE_KEY=
# Shared secret verifying inbound GitHub/GitLab webhooks (min 32 chars).
# You will enter this same value on the GitHub side — see the install guide.
WEBHOOK_SECRET=
# Required broker credentials. Generate independent random values.
# Credentials for the bundled message broker. Internal to this stack — you never enter them
# anywhere else. Keep a letter in each value: the broker's config file reads an all-digit
# credential as a number and refuses to start.
NATS_USERNAME=
NATS_PASSWORD=

Expand Down
8 changes: 7 additions & 1 deletion docker/self-host/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ else
fi
chmod 600 "$working_file"

for key in POSTGRES_PASSWORD HEPHAESTUS_SECURITY_ENCRYPTION_KEY HEPHAESTUS_SECURITY_CREDENTIAL_ENCRYPTION_KEY HEPHAESTUS_AUTH_STATE_COOKIE_KEY WEBHOOK_SECRET; do
for key in POSTGRES_PASSWORD HEPHAESTUS_SECURITY_ENCRYPTION_KEY HEPHAESTUS_SECURITY_CREDENTIAL_ENCRYPTION_KEY HEPHAESTUS_AUTH_STATE_COOKIE_KEY WEBHOOK_SECRET NATS_USERNAME NATS_PASSWORD; do
if [ "$(grep -c "^${key}=" "$working_file")" -gt 1 ]; then
printf 'Refusing duplicate %s assignments in %s.\n' "$key" "$environment_file" >&2
exit 1
Expand All @@ -45,6 +45,10 @@ set_if_empty() {
hex16) value=$(openssl rand -hex 16) || return 1 ;;
hex32) value=$(openssl rand -hex 32) || return 1 ;;
base64) value=$(openssl rand -base64 32 | tr -d '\n') || return 1 ;;
# The broker reads these from its own config file, which interpolates them unquoted;
# its parser reads an all-digit token as a number and exits before it listens. The
# prefix keeps a letter in every generated value.
broker) value=heph$(openssl rand -hex 16) || return 1 ;;
esac
if ! grep -q "^${key}=" "$working_file"; then
printf '%s=%s\n' "$key" "$value" >> "$working_file"
Expand Down Expand Up @@ -84,6 +88,8 @@ if ! grep -q '^HEPHAESTUS_SECURITY_CREDENTIAL_ENCRYPTION_KEY=.' "$working_file";
fi
set_if_empty HEPHAESTUS_AUTH_STATE_COOKIE_KEY base64
set_if_empty WEBHOOK_SECRET hex32
set_if_empty NATS_USERNAME broker
set_if_empty NATS_PASSWORD broker

mv "$working_file" "$environment_file"

Expand Down
5 changes: 3 additions & 2 deletions docs/admin/configuration-readiness.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ application key.

Server and webhook roles require NATS and an explicit `nats://` or `tls://` URI with a host, an optional
valid port, and no query, fragment, or non-root path. A worker-only process must disable NATS because
its job queue is PostgreSQL-backed. The reference deployment requires `NATS_USERNAME` and
`NATS_PASSWORD`. This check validates syntax and role consistency, not connectivity or JetStream health.
its job queue is PostgreSQL-backed. Every deployment requires `NATS_USERNAME` and `NATS_PASSWORD`;
the supported self-host setup generates both. This check validates syntax and role consistency, not
connectivity or JetStream health.

## Login

Expand Down
13 changes: 8 additions & 5 deletions docs/admin/install.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,12 @@ node scripts/prepare-release-lock.ts "v$VERSION"
cd docker/self-host
```

`setup.sh` creates or updates `.env` with mode `0600` and generates the database password,
credential-encryption key, OAuth state-cookie key, and webhook secret. It does not replace non-empty
values and never prints a secret.
`setup.sh` creates or updates `.env` with mode `0600` and generates the database password, both
encryption keys — `HEPHAESTUS_SECURITY_ENCRYPTION_KEY` and
`HEPHAESTUS_SECURITY_CREDENTIAL_ENCRYPTION_KEY` — the OAuth state-cookie key, the webhook secret,
and the message-broker credentials. It does not replace non-empty values and never prints a secret.
Changing either encryption key on an existing installation makes everything encrypted under the old
one unreadable.

Everything below runs from `/opt/hephaestus/docker/self-host`. After step 2 supplies the remaining
required values, `docker compose config` renders the effective configuration. Before then it exits
Expand All @@ -97,8 +100,8 @@ directory; see [Troubleshooting](#troubleshooting).

## 2. Configure `.env`

Open `.env` and fill in the remaining **REQUIRED** values. `setup.sh` manages the four internal
secrets listed above; do not replace them manually.
Open `.env` and fill in the remaining **REQUIRED** values. `setup.sh` manages the internal secrets
listed above; do not replace them manually.

| Variable | What / how |
| --- | --- |
Expand Down
9 changes: 9 additions & 0 deletions scripts/self-host-setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ function managedValues(environment: string): string[] {
"HEPHAESTUS_SECURITY_CREDENTIAL_ENCRYPTION_KEY",
"HEPHAESTUS_AUTH_STATE_COOKIE_KEY",
"WEBHOOK_SECRET",
"NATS_USERNAME",
"NATS_PASSWORD",
]);
return environment
.split("\n")
Expand All @@ -81,6 +83,13 @@ await test("generates protected secrets without printing them", async () => {
assert.equal(credentialKey, encryptionKey);
assert.match(environment, /^HEPHAESTUS_AUTH_STATE_COOKIE_KEY=[A-Za-z0-9+/]{43}=$/m);
assert.match(environment, /^WEBHOOK_SECRET=[0-9a-f]{64}$/m);
// The broker's config file rejects an all-digit credential, so both carry a letter prefix.
assert.match(environment, /^NATS_USERNAME=heph[0-9a-f]{32}$/m);
assert.match(environment, /^NATS_PASSWORD=heph[0-9a-f]{32}$/m);
assert.notEqual(
environment.match(/^NATS_USERNAME=(.+)$/m)?.[1],
environment.match(/^NATS_PASSWORD=(.+)$/m)?.[1],
);
assert.equal((await lstat(environmentPath)).mode & 0o777, 0o600);
for (const secret of managedValues(environment)) assert.ok(!result.output.includes(secret));

Expand Down
Loading