# Configuration readiness

The production profile checks deployment settings at startup. The server-role endpoint `GET /api/admin/configuration-readiness` returns those facts and checks that need runtime state. It requires the `app_admin` authority. It is available only after checks that can prevent startup pass. Diagnostics never contain configured values.

Each fact has these fields:

* A stable `id`.
* The affected configuration `subject`.
* Applicable runtime `roles`.
* A `requirement` and a `status`.
* An explanation and a documentation link.

Requirements are `REQUIRED`, `RECOMMENDED`, or `OPTIONAL`. Status is one of:

| Status            | Meaning                                                                                 |
| ----------------- | --------------------------------------------------------------------------------------- |
| `SATISFIED`       | The applicable check passed.                                                            |
| `ACTION_REQUIRED` | An applicable check failed. Required deployment facts with this status prevent startup. |
| `NOT_CONFIGURED`  | An optional setting is absent.                                                          |
| `NOT_APPLICABLE`  | The check does not apply to this process's runtime roles.                               |

The table names variables inside the application container. In the supported self-host stack, Compose maps `POSTGRES_PASSWORD` to `DATABASE_PASSWORD` and owns the runtime-role values.

| Setting                                         | Application environment variable                |
| ----------------------------------------------- | ----------------------------------------------- |
| `spring.datasource.url`                         | `DATABASE_URL`                                  |
| `spring.datasource.username`                    | `DATABASE_USERNAME`                             |
| `spring.datasource.password`                    | `DATABASE_PASSWORD`                             |
| `hephaestus.runtime.server.enabled`             | `HEPHAESTUS_RUNTIME_SERVER_ENABLED`             |
| `hephaestus.runtime.worker.enabled`             | `HEPHAESTUS_RUNTIME_WORKER_ENABLED`             |
| `hephaestus.runtime.webhook.enabled`            | `HEPHAESTUS_RUNTIME_WEBHOOK_ENABLED`            |
| `hephaestus.host-url`                           | `APPLICATION_HOST_URL`                          |
| `hephaestus.security.encryption-key`            | `HEPHAESTUS_SECURITY_ENCRYPTION_KEY`            |
| `hephaestus.security.credential-encryption-key` | `HEPHAESTUS_SECURITY_CREDENTIAL_ENCRYPTION_KEY` |
| `hephaestus.webhook.secret`                     | `WEBHOOK_SECRET`                                |
| `hephaestus.webhook.routing.secret`             | `WEBHOOK_ROUTING_SECRET`                        |
| `hephaestus.webhook.routing.previous-secret`    | `WEBHOOK_ROUTING_PREVIOUS_SECRET`               |
| `hephaestus.sync.nats.enabled`                  | `NATS_ENABLED`                                  |
| `hephaestus.sync.nats.server`                   | `NATS_SERVER`                                   |
| `hephaestus.sync.nats.username`                 | `NATS_USERNAME`                                 |
| `hephaestus.sync.nats.password`                 | `NATS_PASSWORD`                                 |
| `hephaestus.auth.state-cookie-key`              | `HEPHAESTUS_AUTH_STATE_COOKIE_KEY`              |
| `hephaestus.llm.egress.allow-loopback`          | `HEPHAESTUS_LLM_EGRESS_ALLOW_LOOPBACK`          |
| `hephaestus.agent.image.require-digest`         | `HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST`         |
| `hephaestus.agent.image.reference`              | `HEPHAESTUS_AGENT_IMAGE_REFERENCE`              |
| `hephaestus.sandbox.docker.container-runtime`   | `SANDBOX_DOCKER_CONTAINER_RUNTIME`              |
| `spring.mail.host`                              | `SPRING_MAIL_HOST`                              |
| `hephaestus.email.from`                         | `HEPHAESTUS_EMAIL_FROM`                         |

## Runtime roles

1. Enable at least one of `hephaestus.runtime.server.enabled`, `worker.enabled`, or `webhook.enabled`.
2. Use only `true` or `false` for each value.

The supported split topology enables only webhook on the webhook process and only worker on a remote worker. The application process enables server, optionally with a colocated worker.

## Database

The server and worker roles use PostgreSQL. The webhook role has no database and needs none of these settings. `DATABASE_URL` must be a PostgreSQL URL. The production profile adds the `jdbc:` prefix. Spring Boot and Liquibase perform separate connection and migration health checks. This syntax check does not replace them.

1. Supply a non-empty username.
2. Supply a non-empty password.

Remote databases require TLS. Prefer `sslmode=verify-full`, which encrypts the connection and verifies server identity. `sslmode=require` encrypts without identity verification. Production startup rejects plaintext remote connections unless `HEPHAESTUS_DATABASE_ALLOW_INSECURE_REMOTE=true` explicitly permits them.

## Credential encryption

CAUTION: Preserve encryption keys

Do not change the general encryption key on an existing installation. Rotate the credential key with the documented online procedure.

The server and worker roles require both keys. The webhook role stores no secret and needs neither key.

1. Set each encryption key to exactly 32 printable, non-space ASCII characters.
2. Keep both keys with the backup set, in separate storage with separate credentials. [Keep the required keys outside the cluster](/admin/secret-custody.md#keep-the-required-keys-outside-the-cluster) gives the rule.

The supported self-host setup generates the master key and copies it into an empty credential-key setting. These settings are separate even when their initial values match.

## External URL

Set `hephaestus.host-url` to the public HTTPS origin. The origin must have no credentials, query, fragment, or path other than `/`.

## Webhooks

Server and webhook roles require `hephaestus.webhook.secret` with at least 32 printable, non-space ASCII characters. The supported self-host setup generates an independent value. Never reuse another application key.

The REQUIRED fact `webhook.routing-secret` applies to server and webhook roles on every production deployment, even without GitLab.

1. Set `WEBHOOK_ROUTING_SECRET` to at least 32 printable, non-space ASCII characters.
2. Use a value different from `WEBHOOK_SECRET`.
3. Use the same value on both roles.

## NATS

Server and webhook roles require NATS and an explicit `nats://` or `tls://` URI. The URI must have a host and, optionally, a valid port. It must have no query, fragment, or non-root path. A worker-only process must disable NATS because its job queue uses PostgreSQL.

The supported Compose deployment requires `NATS_USERNAME` and `NATS_PASSWORD`. Setup generates both. Compose, not a readiness fact, requires these credentials. Readiness checks syntax and role consistency, not connectivity or JetStream health.

## Login

The server role requires a Base64-encoded 32-byte `hephaestus.auth.state-cookie-key`. It also requires an enabled GitHub or GitLab sign-in provider in the database-backed provider catalogue. Environment provider entries are seeds, not the readiness authority. Slack and Outline are link-only providers and do not satisfy sign-in readiness. Worker and webhook roles do not load login providers.

Compose does not forward custom auth session or user-view rate-limit variables. To change the properties below, supply an explicit application-server environment or mounted configuration override. A value only in the stack's `.env` does not change these limits.

### Recent sign-in for sensitive actions

These actions require a sign-in from the last few minutes:

* Change an account's role.
* Force an account out of all its sessions.
* View as a user.
* Change a login provider.
* Register an LLM connection.
* Link an identity.

`hephaestus.auth.step-up-max-age` sets that window and defaults to `5m`. It must be a positive duration shorter than the absolute session lifetime. Otherwise, the gate can never bind.

Access confirmation uses a provider already connected to the account. This is local confirmation, not multi-factor authentication. An identity provider with an existing session may complete it without another request to the user. Enforce MFA at the provider.

The server does not replay a refused action after access confirmation.

1. Review the action.
2. Submit it again.

### Session deadlines

The access cookie lasts `24h` by default (`hephaestus.auth.access-ttl`). While active, the browser renews it about hourly, before expiry, or after a return to a tab whose renewal is due. Hidden or inactive tabs do not renew it continuously. This is a renewable credential lifetime, not a precise last-request idle timeout. After the cookie expires, the user must sign in.

In production, tokens stay in HttpOnly, Secure, host-only, SameSite=Lax cookies. The browser never stores them in local storage. A session ends at `hephaestus.auth.session-max-lifetime` (default `7d`), regardless of activity. Access token renewal stays within that deadline, never past it.

Reads through a [user view](/contributor/instance-admin.md#read-only-user-views) have a per-instance-admin budget. `hephaestus.auth.rate-limit.user-view.capacity` per `hephaestus.auth.rate-limit.user-view.period` defaults to 120 per minute. Every read about the viewed member counts, so one page usually uses several. If the rate-limit store is unavailable, the server refuses views rather than permitting them without a budget.

### Choosing session limits

Longer sessions reduce interruptions but increase exposure to stolen cookies and unattended devices. The defaults suit individual devices for routine team work. For shared computers or sensitive deployments, shorten both limits. Require MFA at the identity provider. The recent-sign-in gate above does not establish a second factor.

[NIST SP 800-63B-4 §2.1.3](https://pages.nist.gov/800-63-4/sp800-63b.html#aal1reauth) recommends an overall timeout of no more than 30 days at AAL1. It does not require an inactivity timeout there. [OWASP's session guidance](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#automatic-session-expiration) gives more conservative examples: 15–30 minutes idle and a 4–8 hour overall timeout. These are risk-based reference points, not an assurance-level certification for Hephaestus.

Explicit overrides take precedence over defaults. Renewal applies the configured access lifetime but preserves the session's original absolute deadline and sign-in time. Revocation and account suspension still deny access before either deadline.

Temporary renewal failures leave the page open. They do not bypass server authentication. A failed sign-out reports an error, which the user can retry. If the server rotates a token but its response is lost, the user may need to sign in again. Cross-tab coordination requires [Web Locks support](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API) and a secure context (HTTPS in production).

## LLM proxy

Worker roles must leave `hephaestus.llm.egress.allow-loopback=false`. Provider credentials and model configuration are database-backed runtime configuration and are not deployment settings.

## Agent image

Worker roles require digest enforcement and a SHA-256-pinned `hephaestus.agent.image.reference` (readiness fact `agent.image-contract`). See [Release image lock](/admin/release-image-lock.md).

## Sandbox isolation

Set `SANDBOX_DOCKER_CONTAINER_RUNTIME=runsc` on workers after [installing and configuring gVisor](https://gvisor.dev/docs/user_guide/install/) on the host. This recommendation is non-fatal.

### Docker configuration

For the single-host deployment, keep the local Unix socket default. Docker-specific settings under `hephaestus.sandbox.docker.*` apply to processes with the worker role enabled. The gateway keeps its independent `hephaestus.sandbox.gateway.*` settings. Resource limits, capacity, and workspace transfer limits remain under `hephaestus.sandbox.*`.

| Property under `hephaestus.sandbox.docker` | Environment variable               | Default                       |
| ------------------------------------------ | ---------------------------------- | ----------------------------- |
| `host`                                     | `SANDBOX_DOCKER_HOST`              | `unix:///var/run/docker.sock` |
| `tls-verify`                               | `SANDBOX_DOCKER_TLS_VERIFY`        | `false`                       |
| `cert-path`                                | `SANDBOX_DOCKER_CERT_PATH`         | unset                         |
| `container-runtime`                        | `SANDBOX_DOCKER_CONTAINER_RUNTIME` | Docker daemon default         |
| `owner`                                    | `SANDBOX_DOCKER_OWNER`             | `default`                     |

Compose forwards these variables only to `application-worker`. `application-server` runs without the worker role. Each worker joins a job network or its conversation host network as the container Docker identifies by its default hostname. Keep the default hostname of each worker container. With a custom `hostname:`, the worker logs that it cannot identify its container, and it starts no conversation host.

A worker outside Docker joins no job network. The server refuses its practice reviews, which run on an internal network. The worker also logs that it cannot identify its container, but it starts its conversation host ([runtime roles](/admin/runtime-roles.md#the-conversation-host)).

Use a distinct `owner` for each installation that shares a Docker daemon but uses a different database. All worker-capable roles of the same installation must use the same stable value. It accepts 1–63 lowercase letters, digits, and hyphens, with a letter or digit first. Container inventory, capacity accounting, and cleanup select the `hephaestus.sandbox-owner` label.

Job networks and conversation host networks use `hephaestus-sandbox-<owner>--<UUID>`. For a conversation host, the UUID is the host ID. This prevents accidental cross-installation cleanup. It is not an authorization boundary against a process with Docker daemon access.

Each worker starts one conversation host, with its own internal network, attempt volumes, and container. These resources have the label `hephaestus.session-id=<host-id>`. The host container also has the label `hephaestus.kind=heph-host`.

These resources also record the worker container that started them, identified by its own `HOSTNAME`, and its start time. Cleanup removes them only after that worker container stops or restarts. It keeps resources whose creator it cannot establish.

After a crash of that worker, its conversation host exits when its command channel closes. Cleanup removes the stopped host container and its other resources after the worker container stops or restarts. A worker outside Docker records no creator, so the resources of its conversation host require manual removal. A new host gets a new ID, so leftover resources do not stop new conversation turns. Cleanup also keeps networks created before this record existed.

The REQUIRED fact `sandbox.docker-legacy-configuration` rejects old Docker settings outside `hephaestus.sandbox.docker.*` on workers. Move them to the property names above. The worker does not read the old settings.

#### Removing leftover sandbox networks

CAUTION: Establish network ownership before removal

A sandbox network's name identifies only its job or conversation host, not whether the network is unused. A conversation host can take up to its start timeout to start ([runtime roles](/admin/runtime-roles.md#the-conversation-host)). During that time, only the worker container may be attached. Establish the owner before you remove the network.

1. Read the recorded creator:

   ```
   docker network inspect --format '{{index .Labels "hephaestus.creator-container"}} {{index .Labels "hephaestus.creator-started-at"}}' <network>
   ```

2. If a creator is recorded, check it:

   ```
   docker container inspect --format '{{.State.Running}} {{.State.StartedAt}}' <creator>
   ```

   If it runs with the recorded start time, it still owns the network. Leave that network. If the creator no longer exists, stopped, or restarted, automatic cleanup removes the network on its next pass. No manual action is necessary.

3. If the creator record is absent or unreadable, stop this installation's application processes on this Docker daemon. Include the application server and workers with this `owner`. Only this pause proves the network unused: no process can start or attach a sandbox. Other installations on the daemon are unaffected.

4. Only after that pause, remove the conversation host containers that the stopped workers left:

   ```
   docker container ls -aq --filter label=hephaestus.kind=heph-host --filter label=hephaestus.sandbox-owner=<owner>

   docker container rm --force <container>
   ```

5. Inspect the network again:

   ```
   docker network inspect --format '{{range $id, $c := .Containers}}{{$id}} {{$c.Name}}{{println}}{{end}}' <network>
   ```

6. Resolve the worker container to its full id:

   ```
   docker container inspect --format '{{.Id}}' <worker-container>
   ```

   If an attached container has another id, a sandbox still uses the network. Leave that network.

7. If the worker container is still attached, disconnect it by its full id:

   ```
   docker network disconnect <network> <worker-id>
   ```

8. Remove the network:

   ```
   docker network rm <network>
   ```

9. List the host's attempt volumes, using the host ID at the end of the network name:

   ```
   docker volume ls -q --filter label=hephaestus.session-id=<host-id>
   ```

10. Remove those volumes:

    ```
    docker volume rm
    ```

    The command refuses a volume that a container still uses.

Before an upgrade from unscoped sandbox ownership or a change to `owner`:

1. Drain reviews and conversation turns.
2. Stop that installation's workers.
3. Verify ownership of its remaining containers and networks.
4. Only after verification, remove those resources.

Cleanup never adopts containers labelled `hephaestus.managed=true`, `agent-net-` networks, or resources of a previous owner. All Docker operations of the worker use these host, TLS, and certificate settings. Inherited `DOCKER_CONTEXT`, `DOCKER_HOST`, and Docker TLS environment variables do not override them.

A TCP connection changes how the worker reaches Docker, not where its containers run. The daemon must manage the worker container and its sandbox networks.

1. Follow [Docker's mutual-TLS setup](https://docs.docker.com/engine/security/protect-access/).
2. Set `SANDBOX_DOCKER_TLS_VERIFY=true`.
3. With a Compose override, mount client certificates read-only into each worker-capable container.
4. Set `SANDBOX_DOCKER_CERT_PATH` to that **container** directory, which must contain `ca.pem`, `cert.pem`, and `key.pem`.

A host path in `.env` does not mount it. Never mount the Docker socket or client certificates in a sandbox.

These conditions prevent worker startup:

* Unknown keys under `hephaestus.sandbox.docker`.
* Blank `host` or `owner`.
* TLS verification without a certificate path.

These checks do not verify daemon connectivity or runtime availability. See [runtime roles](/admin/runtime-roles.md) for the gateway boundary.

The application cannot harden the Docker host from inside a container. The supported host matrix's [Docker Engine version](/admin/install.md#supported-host-matrix) already closes CVE-2024-29018's DNS-exfiltration path. For further host protection:

1. Block the cloud metadata endpoint on the host:

   ```
   iptables -I DOCKER-USER -d 169.254.0.0/16 -j DROP
   ```

2. Recommended: put a proxy such as Tecnativa's `docker-socket-proxy`, configured with `EXEC=0`, in front of the daemon socket.

3. Recommended: set `userns-remap=default` in `/etc/docker/daemon.json`.

## Email

Email is optional. The fact `notification.email` is `NOT_CONFIGURED` until `spring.mail.host` names a relay. It is `ACTION_REQUIRED` if a relay is set but `hephaestus.email.from` is blank. Without a sender address, every notification is withheld and recorded as `NOT_CONFIGURED`. This optional readiness fact never prevents startup.

Personal settings do not expose SMTP setup warnings. Instance admins configure the capability in instance settings. See the supported no-email behavior in the setup guide. [Integration settings → Email](/admin/production-setup.md#email) gives configuration and verification instructions. Before email activation, complete the processing review in the [processor checklist](/admin/dsms/processor-checklist.md#follow-up-if-the-processing-surface-changes).

### Test-instance confirmation policy

[Admin passkeys](/admin/passkeys.md#test-instances-without-additional-confirmation) explains the explicit test-instance override for additional authentication checks. Keep its default enabled on production instances.
