Implement supported ingress and TLS profiles
Dependency Audit / dependency-audit (push) Successful in 1m38s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m2s

This commit is contained in:
2026-08-03 01:15:06 +02:00
parent b40f1428fd
commit 2c515f73c2
12 changed files with 1174 additions and 37 deletions
@@ -91,6 +91,8 @@ The private installation directory contains:
| `compose.json` | Deterministic generated Compose definition |
| `garage.toml` | Non-secret managed Garage server configuration |
| `load-balancer.cfg` | Non-secret HAProxy WebUI/API discovery configuration |
| `Caddyfile` | Non-secret managed-ingress route and ACME policy |
| `existing-proxy.json` | Exact upstream, trusted-source, header, and health contract for an operator-owned proxy |
| `plan.json` | Latest desired-state diff and readiness findings |
| `receipt.json` | Last successfully applied immutable identities |
| `distribution-manifest.json` | Canonical signed runtime/image selection adopted by the installer |
@@ -184,12 +186,11 @@ a production distribution:
5. **Deployment agent.** Web updates need a separate privileged reconciler with
a typed command allowlist. The API and browser must never receive the Docker
socket or arbitrary shell access.
6. **Ingress and certificates.** The managed HAProxy service provides HTTP
load balancing inside the deployment boundary; it does not issue or renew
certificates. A self-hosted profile still needs an explicit choice
between an existing reverse proxy and a supported managed ingress, including
trusted-proxy boundaries, TLS certificate issuance, renewal, and health
probing through the public route.
6. **Ingress reachability evidence.** Managed Caddy ingress and the
existing-proxy contract are implemented. A production claim still requires
running `doctor` from the target host after public DNS/firewall changes and
retaining the first successful container drill and public TLS/readiness
evidence.
`apply --allow-unverified-images` is therefore restricted to the evaluation
profile. It explicitly acknowledges both mutable image identities and
@@ -263,7 +264,9 @@ must use a tested multi-node Garage cluster or another external S3 service.
### Load Balancing And Replicas
The generated Compose topology publishes only `load-balancer`. HAProxy uses
The generated Compose topology publishes only `load-balancer` for local or
existing-proxy profiles. With managed ingress, only Caddy publishes host ports
and HAProxy remains private. HAProxy uses
Docker DNS service discovery to distribute public traffic across WebUI replicas
and WebUI API proxy traffic across API replicas. The WebUI and API services do
not publish host ports. HAProxy has no Docker socket and discovers only the
@@ -287,6 +290,49 @@ PostgreSQL advisory lock. The Celery scheduler is run under a renewable,
fencing-token lease. Multiple API replicas are rejected when Redis is disabled
because distributed throttling and queued work cannot then be shared correctly.
### Public Ingress And TLS
A self-hosted installation is fail-closed until one of these boundaries is
selected:
- `existing-proxy` publishes HAProxy at `listen.address:listen.port` and emits
`existing-proxy.json`. The operator-owned proxy must use the recorded host,
upstream, and health paths. Only the exact CIDRs listed with repeated
`--trusted-proxy-cidr` values may supply `X-Forwarded-*` headers. Public
proxy addresses must be `/32` or `/128`; private ranges are limited to `/24`
or narrower for IPv4 and `/64` or narrower for IPv6.
- `managed` publishes Caddy on the selected HTTP/HTTPS ports, redirects HTTP to
HTTPS, obtains and renews certificates through ACME, and keeps certificate
material exclusively in the private `caddy-data` and `caddy-config` volumes.
The application containers receive no ACME account or TLS private keys.
Example existing-proxy configuration:
```sh
python govoplan-deploy.py configure \
--directory /srv/govoplan \
--ingress existing-proxy \
--trusted-proxy-cidr 172.20.0.7/32
```
Example managed configuration:
```sh
python govoplan-deploy.py configure \
--directory /srv/govoplan \
--ingress managed \
--acme-email operator@example.org
```
Before managed ingress starts, public A/AAAA records must resolve to the target
and inbound TCP 80/443 must reach it. Existing-proxy mode additionally requires
the public proxy and valid certificate to be reachable before apply. After a
successful receipt, `doctor` reports DNS resolution, certificate validity and
remaining lifetime, public `/health/ready`, and the private HAProxy/WebUI path
as separate checks. Reconfiguration retains the certificate volumes; bundle
rollback never deletes or exposes their contents. Include both Caddy volumes
in coordinated backup and restore evidence.
This is same-host scaling. Docker Compose uses a bridge network and does not
place containers on another machine. See
[Scaling And Multi-Host Deployment](SCALING_AND_MULTI_HOST_DEPLOYMENT.md) for
+1 -1
View File
@@ -193,7 +193,7 @@ fencing. It does not by itself provide:
- automatic PostgreSQL backup, point-in-time recovery, or restore verification;
- autoscaling policy;
- central logs, metrics, traces, or alert routing;
- managed ingress certificates;
- certificate portability between independently managed ingress providers;
- automatic reconciliation of every possible module side effect;
- a service-level availability guarantee.
+49
View File
@@ -177,6 +177,55 @@
}
}
},
"ingress": {
"type": "object",
"additionalProperties": false,
"required": [
"mode",
"image",
"trusted_proxy_cidrs",
"http_port",
"https_port",
"acme_email"
],
"properties": {
"mode": {
"enum": [
"local",
"existing-proxy",
"managed",
"unconfigured"
]
},
"image": {
"type": "string",
"maxLength": 300
},
"trusted_proxy_cidrs": {
"type": "array",
"maxItems": 16,
"uniqueItems": true,
"items": {
"type": "string",
"maxLength": 64
}
},
"http_port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"https_port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"acme_email": {
"type": "string",
"maxLength": 254
}
}
},
"enabled_modules": {
"type": "array",
"uniqueItems": true,