Skip to content

Commit 311763a

Browse files
feat(server): validate production configuration on startup
1 parent ca0c5af commit 311763a

24 files changed

Lines changed: 909 additions & 14 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hephaestus": minor
3+
---
4+
5+
Production startup now reports all boot-critical configuration problems together without exposing configured values, and instance administrators can inspect redacted deployment and runtime readiness facts through the API. **Operators:** validate production settings against the configuration readiness guide before upgrading; the process now refuses to start when a required setting is missing or invalid.

MIGRATION.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ review runs and no feedback is prepared about them.
8282
**Operator action after upgrading**: open **Practices → Review → When and where** and confirm the
8383
**People** and **Repositories** counts. Existing members need no action. If expected contributors are
8484
missing, follow [Who counts as a person](https://ls1intum.github.io/Hephaestus/admin/practice-review#who-counts-as-a-person).
85+
#### 🔴 Production configuration is validated before startup
86+
87+
**Affected**: deployments that activate the `prod` Spring profile and have a missing, malformed, or
88+
role-inconsistent required setting.
89+
90+
Production processes now validate all applicable boot-critical settings together and refuse to
91+
start until every reported error is resolved. The failure report identifies properties and
92+
documentation but never includes configured values. Development and test-only profiles retain their
93+
existing defaults.
94+
95+
**Action**: before upgrading, compare every production role's settings with the
96+
[configuration readiness guide](https://ls1intum.github.io/Hephaestus/admin/configuration-readiness).
97+
Correct every required setting reported during a staging startup, then deploy the same configuration
98+
to production. After the server role starts, an instance administrator can inspect the redacted
99+
facts through `GET /admin/configuration-readiness`.
100+
85101
#### 🔴 Practice area API names are replaced by practice group names
86102

87103
**Affected**: anything calling the application API directly. The generated Hephaestus web client is
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
title: Configuration readiness
3+
---
4+
5+
# Configuration readiness
6+
7+
The production profile validates deployment settings before application beans are created. The
8+
server-role endpoint `GET /api/admin/configuration-readiness` returns those facts plus checks that need
9+
runtime state. It requires the `app_admin` authority and is available only when boot-fatal checks pass.
10+
Diagnostics never contain configured values.
11+
12+
| Setting | Deployment environment variable | Accepted value |
13+
| --- | --- | --- |
14+
| `hephaestus.host-url` | `APPLICATION_HOST_URL` | Root HTTPS origin |
15+
| `hephaestus.security.encryption-key` | `HEPHAESTUS_SECURITY_ENCRYPTION_KEY` | Exactly 32 ASCII bytes |
16+
| `hephaestus.webhook.secret` | `WEBHOOK_SECRET` | At least 32 ASCII characters |
17+
| `hephaestus.sync.nats.enabled` | `NATS_ENABLED` | `true` on server/webhook; `false` on worker-only |
18+
| `hephaestus.sync.nats.server` | `NATS_SERVER` | `nats://` or `tls://` URI |
19+
| `hephaestus.auth.state-cookie-key` | `HEPHAESTUS_AUTH_STATE_COOKIE_KEY` | Base64-encoded 32-byte key |
20+
| `hephaestus.agent.image.require-digest` | `HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST` | `true` on workers |
21+
| `hephaestus.agent.image.reference` | `HEPHAESTUS_AGENT_IMAGE_REFERENCE` | SHA-256 digest reference |
22+
| `hephaestus.sandbox.container-runtime` | `SANDBOX_CONTAINER_RUNTIME` | `runsc` recommended |
23+
24+
## Runtime roles {#runtime-roles}
25+
26+
Enable at least one of `hephaestus.runtime.server.enabled`, `worker.enabled`, or `webhook.enabled`.
27+
The supported split topology enables only webhook on the webhook process, only worker on a remote
28+
worker, and server (optionally with a colocated worker) on the application process.
29+
30+
## Database {#database}
31+
32+
Every role uses PostgreSQL. Set `spring.datasource.url` to a PostgreSQL JDBC URL and supply a non-empty
33+
username and password. A successful static check does not replace the connection and migration health
34+
checks performed by Spring Boot and Liquibase.
35+
36+
## Credential encryption {#credential-encryption}
37+
38+
Set `hephaestus.security.encryption-key` to exactly 32 ASCII bytes and keep it with the database backup.
39+
Generate it with `openssl rand -base64 24 | cut -c1-32`. Do not change it on an existing installation.
40+
41+
## External URL {#external-url}
42+
43+
Set `hephaestus.host-url` to the public HTTPS origin, without credentials, a path other than `/`, a
44+
query, or a fragment.
45+
46+
## Webhooks {#webhooks}
47+
48+
Server and webhook roles require `hephaestus.webhook.secret` with at least 32 ASCII characters. Generate
49+
an independent value with `openssl rand -hex 32`; never reuse another application key.
50+
51+
## NATS {#nats}
52+
53+
Server and webhook roles require NATS and an explicit `nats://` or `tls://` server URI. A worker-only
54+
process must disable NATS because its job queue is PostgreSQL-backed.
55+
This check validates syntax and role consistency, not authentication, connectivity, or JetStream health.
56+
57+
## Login {#login}
58+
59+
The server role requires a Base64-encoded 32-byte `hephaestus.auth.state-cookie-key` and an enabled
60+
GitHub or GitLab sign-in provider in the database-backed provider catalogue. Environment provider
61+
entries are seeds, not the readiness authority. Slack and Outline are link-only providers and do not
62+
satisfy sign-in readiness. Worker and webhook roles do not load login providers.
63+
64+
## LLM proxy {#llm-proxy}
65+
66+
Worker roles must leave `hephaestus.llm.egress.allow-loopback=false`. Provider credentials and model
67+
configuration are database-backed runtime configuration and are not deployment settings.
68+
69+
## Agent image {#agent-image}
70+
71+
Worker roles require digest enforcement and a SHA-256-pinned `hephaestus.agent.image.reference`. See
72+
[Agent image digests](./agent-image-digests.md).
73+
74+
## Sandbox isolation {#sandbox-isolation}
75+
76+
Set `SANDBOX_CONTAINER_RUNTIME=runsc` on workers after
77+
[installing and configuring gVisor](https://gvisor.dev/docs/user_guide/install/) on the host. This
78+
recommendation is non-fatal.
79+
80+
## Optional observability {#optional-observability}
81+
82+
Sentry is optional. When configured, `hephaestus.sentry.dsn` must use HTTPS. The fact is classified
83+
`OPTIONAL` and never prevents startup.

docs/sidebars.admin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const sidebars: SidebarsConfig = {
2020
{ type: "doc", id: "production-setup", label: "Integrations & Reference Deployment" },
2121
{ type: "doc", id: "compatibility-policy", label: "Compatibility Policy" },
2222
{ type: "doc", id: "runtime-roles", label: "Runtime Roles" },
23+
{ type: "doc", id: "configuration-readiness", label: "Configuration Readiness" },
2324
{ type: "doc", id: "agent-image-digests", label: "Agent image digests" },
2425
{ type: "doc", id: "buildpacks-cds-decision", label: "Server image build (Buildpacks + CDS)" },
2526
{ type: "doc", id: "legal-pages", label: "Legal Pages" },

server/application/src/main/java/de/tum/cit/aet/hephaestus/core/auth/provider/LoginProviderClientRegistrationRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.time.Duration;
88
import java.util.Iterator;
99
import java.util.List;
10+
import java.util.Set;
1011
import org.jspecify.annotations.Nullable;
1112
import org.springframework.security.oauth2.client.registration.ClientRegistration;
1213
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
@@ -86,6 +87,12 @@ public List<ClientRegistration> listRegistrations() {
8687
.toList();
8788
}
8889

90+
@Override
91+
public boolean hasEnabledPrimarySignInProvider() {
92+
return loginProviderRepository.existsByEnabledTrueAndTypeIn(
93+
Set.of(LoginProvider.ProviderType.GITHUB, LoginProvider.ProviderType.GITLAB));
94+
}
95+
8996
private boolean isApproved(LoginProvider provider) {
9097
return (provider.getType() != LoginProvider.ProviderType.OUTLINE
9198
|| outlineOriginPolicy.allows(provider.getBaseUrl()));

server/application/src/main/java/de/tum/cit/aet/hephaestus/core/auth/provider/LoginProviderRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package de.tum.cit.aet.hephaestus.core.auth.provider;
22

33
import de.tum.cit.aet.hephaestus.core.WorkspaceAgnostic;
4+
import java.util.Collection;
45
import java.util.List;
56
import java.util.Optional;
67
import org.springframework.data.jpa.repository.JpaRepository;
@@ -22,4 +23,6 @@ public interface LoginProviderRepository extends JpaRepository<LoginProvider, Lo
2223

2324
/** Enabled providers, ordered for a stable login-page display. */
2425
List<LoginProvider> findByEnabledTrueOrderByDisplayNameAsc();
26+
27+
boolean existsByEnabledTrueAndTypeIn(Collection<LoginProvider.ProviderType> types);
2528
}

server/application/src/main/java/de/tum/cit/aet/hephaestus/core/auth/spi/IdentityProviderCatalog.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,12 @@
33
import java.util.List;
44
import org.springframework.security.oauth2.client.registration.ClientRegistration;
55

6-
/**
7-
* Enumerates every enabled sign-in option a user may pick, drawn from the instance-scoped
8-
* {@code login_provider} store. Backs the public identity-provider discovery endpoint.
9-
*
10-
* <p>Implemented by {@code core.auth.provider.LoginProviderClientRegistrationRepository} (the
11-
* DB-backed {@code ClientRegistrationRepository} that displaces Boot's in-memory bean); consumed by
12-
* {@code core.auth}'s discovery controller through this port. Returns Spring Security
13-
* {@link ClientRegistration}s — a framework-neutral type both sides already speak.
14-
*/
6+
/** Provides enabled OAuth registrations and primary sign-in capability. */
157
public interface IdentityProviderCatalog {
168
/**
179
* @return the client registrations for all enabled login providers. Never {@code null}.
1810
*/
1911
List<ClientRegistration> listRegistrations();
12+
13+
boolean hasEnabledPrimarySignInProvider();
2014
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package de.tum.cit.aet.hephaestus.core.configuration;
2+
3+
import java.util.Set;
4+
import org.jspecify.annotations.NonNull;
5+
6+
public record ConfigurationFactDTO(
7+
@NonNull String id,
8+
@NonNull String subject,
9+
@NonNull Set<ConfigurationRole> roles,
10+
@NonNull ConfigurationRequirement requirement,
11+
@NonNull ConfigurationStatus status,
12+
@NonNull String explanation,
13+
@NonNull String documentationUrl) {}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package de.tum.cit.aet.hephaestus.core.configuration;
2+
3+
import de.tum.cit.aet.hephaestus.core.WorkspaceAgnostic;
4+
import de.tum.cit.aet.hephaestus.core.auth.spi.IdentityProviderCatalog;
5+
import de.tum.cit.aet.hephaestus.core.runtime.ConditionalOnServerRole;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.tags.Tag;
8+
import java.util.List;
9+
import org.springframework.core.env.Environment;
10+
import org.springframework.security.access.prepost.PreAuthorize;
11+
import org.springframework.web.bind.annotation.GetMapping;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.RestController;
14+
15+
@ConditionalOnServerRole
16+
@RestController
17+
@WorkspaceAgnostic("Redacted instance configuration facts, app_admin only")
18+
@RequestMapping("/admin/configuration-readiness")
19+
@Tag(name = "Configuration Readiness", description = "Redacted production configuration facts")
20+
@PreAuthorize("hasAuthority('app_admin')")
21+
public class ConfigurationReadinessController {
22+
23+
private final ConfigurationReadinessEvaluator evaluator;
24+
private final IdentityProviderCatalog identityProviderCatalog;
25+
26+
ConfigurationReadinessController(Environment environment, IdentityProviderCatalog identityProviderCatalog) {
27+
this.evaluator = new ConfigurationReadinessEvaluator(environment);
28+
this.identityProviderCatalog = identityProviderCatalog;
29+
}
30+
31+
@GetMapping
32+
@Operation(summary = "Get redacted configuration readiness", operationId = "adminGetConfigurationReadiness")
33+
public List<ConfigurationFactDTO> get() {
34+
return evaluator.evaluateReadiness(identityProviderCatalog.hasEnabledPrimarySignInProvider());
35+
}
36+
}

0 commit comments

Comments
 (0)