ecdba833d9
New 'orca doctor ingress' command: verifies podman orca-traefik
container running, nft DNAT+SNAT, /etc/traefik/dynamic exists,
step-ca root CA present.
UAT signoff script: replaced assertion 36 (systemd → podman
container), added assertions 40-46 (nft table, DNAT, SNAT, dynamic
dir, step-ca CA, traefik.yml, doctor ingress pass).
docs/ingress.md: R-024 podman traefik section — three topologies,
container config, nft ruleset, doctor ingress, Dockerfile.traefik.
TLS model updated (drop certResolver, tls:{} for v0.14, mTLS v0.15).
ARCHITECTURE.md: v0.14 deltas section — R-024, three topologies,
nft emitter changes, TLS model, migration 0009, new CLI.
Integration tests (tests/ingress_bootstrap_test.go): nft postrouting
+ DNATTarget, priority -10, traefik TLS model (tls:{} no
certResolver), image ref resolution, floating-IP LXC provisioning
commands (pct create with hwaddr/ip/gw/features), MAC generation.
---ci---
project: orca
phase: 7
milestone: v0.14
status: execute
---/ci---
273 lines
8.9 KiB
Markdown
273 lines
8.9 KiB
Markdown
# Orca Ingress & Traefik Guide
|
|
|
|
This document explains how Orca configures ingress via Traefik dynamic
|
|
configuration. It covers the service→Traefik mapping, the R-007
|
|
socket-vs-TCP-bind model, atomic reload, drain, TLS, and a worked
|
|
example.
|
|
|
|
> **Canonical path (v0.9)**: Orca generates Traefik dynamic
|
|
> configuration files via the `TraefikEmitter`. The `kind: Service`
|
|
> workload implies a Traefik route. The emitter renders one YAML file
|
|
> per Service; Traefik watches the dynamic config directory and reloads
|
|
> atomically on change.
|
|
|
|
## The model
|
|
|
|
A `kind: Service` jobspec **implies** a Traefik route (D-175). `Job`
|
|
and `DaemonSet` do **not** carry a Traefik route by default — a
|
|
`service:` block on a `Job` is rejected by the validator.
|
|
|
|
When `orca job run` submits a `kind: Service` workload, the
|
|
`TraefikEmitter` renders a Traefik dynamic config file at:
|
|
|
|
```
|
|
/etc/traefik/dynamic/orca-<service-name>.yaml
|
|
```
|
|
|
|
This file contains:
|
|
- One **router** (`orca-<name>`) with a `PathPrefix` rule and TLS config.
|
|
- One **service** (`orca-<name>`) as a `loadBalancer` with one **server**
|
|
per port, pointing at the workload's Unix socket (or TCP port).
|
|
- A **healthCheck** stanza when the `health:` block is present.
|
|
|
|
Traefik watches `/etc/traefik/dynamic/` via `fsnotify` and reloads
|
|
whenever a file changes. Orca writes config atomically (write-tmp +
|
|
rename) so Traefik sees a single `IN_MOVED_TO` event and never observes
|
|
a half-written file.
|
|
|
|
## R-007: socket vs TCP bind
|
|
|
|
Orca workloads bind to a **Unix socket** by default, not a TCP port.
|
|
This is the R-007 security model: loopback-only by default, no network
|
|
exposure.
|
|
|
|
### Default: Unix socket
|
|
|
|
When `service.bind` is empty (default), the workload binds a Unix
|
|
socket at:
|
|
|
|
```
|
|
/run/orca/alloc-<alloc-id>/port-<port-name>.sock
|
|
```
|
|
|
|
systemd creates `/run/orca/alloc-<alloc-id>/` via
|
|
`RuntimeDirectory=orca/alloc-<alloc-id>` (mode 0750, owned by
|
|
`orca:orca`). The Traefik backend server URL is:
|
|
|
|
```yaml
|
|
servers:
|
|
- url: "unix:///run/orca/alloc-<alloc-id>/port-<port-name>.sock"
|
|
```
|
|
|
|
### TCP opt-in: `service.bind: 127.0.0.1`
|
|
|
|
When `service.bind: 127.0.0.1` is set, the workload binds a TCP port
|
|
directly (loopback only). The emitter adds an `ExecStartPre` marker to
|
|
the systemd unit so the bind mode is visible:
|
|
|
|
```ini
|
|
ExecStartPre=/bin/echo orca: bind 127.0.0.1 port <name> (tcp, R-007 opt-in)
|
|
```
|
|
|
|
`service.bind` must be a valid IP address. Empty (socket default) or
|
|
`127.0.0.1` (TCP opt-in) are the documented values; any other valid IP
|
|
is accepted but the bind happens in the process, not the emitter.
|
|
|
|
## Generated Traefik YAML
|
|
|
|
For a Service named `web` with port `http`:
|
|
|
|
```yaml
|
|
http:
|
|
routers:
|
|
orca-web:
|
|
rule: PathPrefix("/web")
|
|
service: orca-web
|
|
tls:
|
|
certResolver: orca
|
|
domains:
|
|
- main: "cluster.orca.local"
|
|
services:
|
|
orca-web:
|
|
loadBalancer:
|
|
servers:
|
|
- url: "unix:///run/orca/alloc-<alloc-id>/port-http.sock"
|
|
healthCheck:
|
|
path: /healthz
|
|
interval: 5s
|
|
timeout: 1s
|
|
```
|
|
|
|
- One router per Service, named `orca-<service-name>`.
|
|
- Router rule: `PathPrefix("/<service-name>")`.
|
|
- TLS: `certResolver: orca`, trust domain `cluster.orca.local`
|
|
(placeholder; step-ca provisioner overrides in v0.11).
|
|
- One service per Service, named `orca-<service-name>`.
|
|
- One server per port, URL is `unix://<socket-path>`.
|
|
- `healthCheck` stanza present when `health:` block is set (required
|
|
for Service). Path is `/healthz`; interval and timeout come from the
|
|
`health:` block.
|
|
|
|
## Atomic reload (gate C-10)
|
|
|
|
Orca writes Traefik config atomically to avoid Traefik observing a
|
|
half-written file:
|
|
|
|
1. Write to `<path>.tmp` via `WriteFileIdempotent` (write + fsync).
|
|
2. `mv -f <path>.tmp <path>` (atomic POSIX rename).
|
|
|
|
Traefik's `fsnotify` watcher sees a single `IN_MOVED_TO` event and
|
|
reloads. If the new config is malformed, Traefik logs an error and
|
|
**holds last-good config** — the cluster keeps serving traffic on the
|
|
previous config.
|
|
|
|
## Drain
|
|
|
|
`RenderDrain` produces the same Traefik YAML with `weight: 0` on every
|
|
server in the load balancer:
|
|
|
|
```yaml
|
|
servers:
|
|
- url: "unix:///run/orca/alloc-<alloc-id>/port-http.sock"
|
|
weight: 0
|
|
```
|
|
|
|
Traefik stops sending traffic to the drained backend. The workload
|
|
keeps running; drain is reversible (re-submit the normal config to
|
|
restore traffic).
|
|
|
|
## TLS
|
|
|
|
- **v0.14 model**: `tls: {}` in dynamic config (no certResolver).
|
|
Traefik v3.3 `certificatesResolvers` only supports `acme` and
|
|
`tailscale` — not CA-file-based. The `certResolver: orca` reference
|
|
from v0.11 was broken (research finding). v0.14 emits `tls: {}`
|
|
(traefik uses its default self-signed cert). Real mTLS via dynamic
|
|
`tls.certificates` + `tls.options.default.clientAuth.caFiles` is
|
|
deferred to v0.15.
|
|
- **Step-ca root CA**: mounted at `/etc/orca/step-ca-root.crt` in the
|
|
traefik container. v0.14 does not use it for TLS termination (it's
|
|
a placeholder for v0.15 mTLS).
|
|
|
|
## R-024: Podman Traefik Container (v0.14)
|
|
|
|
As of v0.14, Traefik runs as a **podman container** from the custom
|
|
`orca-traefik` image (published per release). The v0.13 binary+systemd
|
|
install is replaced.
|
|
|
|
### Three topologies
|
|
|
|
1. **Linux**: host → nft DNAT → `podman run orca-traefik` (`--network host`)
|
|
2. **Proxmox Native** (`--ingress-mode native`, default): PVE host →
|
|
nft DNAT → LXC (nesting=1,keyctl=1,fuse=1) → `podman run orca-traefik`
|
|
3. **Proxmox Floating-IP** (`--ingress-mode floating-ip`): LXC owns
|
|
the floating IP → nft inside LXC → `podman run orca-traefik`
|
|
|
|
### Container configuration
|
|
|
|
```bash
|
|
podman run -d --name orca-traefik --restart=unless-stopped \
|
|
--network host \
|
|
-v /etc/traefik/traefik.yml:/etc/traefik/traefik.yml:ro \
|
|
-v /etc/traefik/dynamic:/etc/traefik/dynamic:ro \
|
|
-v /etc/orca/step-ca-root.crt:/etc/orca/step-ca-root.crt:ro \
|
|
git.cloudinit.dev/coreci/orca-traefik:<version>
|
|
```
|
|
|
|
- `--network host`: traefik binds 127.0.0.1:8080/8443 on host/LXC loopback
|
|
- `--restart=unless-stopped`: survives reboot via `podman-restart.service`
|
|
- No `:Z` SELinux flag (research Topic 7)
|
|
- Static config mounted `:ro` (overrides baked image default, preserves
|
|
`traefik-on-public-ip` opt-out, REQ-100)
|
|
|
|
### nft ruleset
|
|
|
|
The nft emitter (`internal/emitter/nft.go`) renders `/etc/nftables.d/orca.nft`:
|
|
|
|
- DNAT `:443` → `<DNATTarget>:8443` (default 127.0.0.1; LXC IP for native)
|
|
- DNAT `:80` → `<DNATTarget>:8080`
|
|
- SNAT/MASQUERADE: `ip saddr 127.0.0.0/8 oifname != "lo" masquerade`
|
|
- Input/forward chains at priority -10 (pve-firewall coexistence)
|
|
|
|
### `orca doctor ingress`
|
|
|
|
```bash
|
|
orca doctor ingress # check localhost
|
|
orca doctor ingress --peer <name> # check remote peer
|
|
```
|
|
|
|
Verifies: podman container running, nft DNAT+SNAT, dynamic dir exists,
|
|
step-ca root CA present.
|
|
|
|
### Dockerfile.traefik
|
|
|
|
```dockerfile
|
|
FROM traefik:v3.3.0
|
|
COPY docker/orca-traefik/traefik.yml /etc/traefik/traefik.yml
|
|
CMD ["--configFile=/etc/traefik/traefik.yml"]
|
|
```
|
|
|
|
Built + published per release alongside the orca image
|
|
(`scripts/release.sh` + `.coreci.yml container-publish-traefik`).
|
|
|
|
## Health checks
|
|
|
|
The `health:` block (required for `Service`) maps to the Traefik
|
|
`healthCheck` stanza:
|
|
|
|
```yaml
|
|
health:
|
|
check_type: http
|
|
interval: 5s
|
|
timeout: 1s
|
|
unhealthy_threshold: 2
|
|
```
|
|
|
|
→
|
|
|
|
```yaml
|
|
healthCheck:
|
|
path: /healthz
|
|
interval: 5s
|
|
timeout: 1s
|
|
```
|
|
|
|
Traefik polls each backend's `/healthz` at the configured interval. An
|
|
unhealthy backend is removed from the load balancer pool until it
|
|
passes the health check again.
|
|
|
|
## Worked example
|
|
|
|
See [examples/full-stack/](../examples/full-stack/) for a complete
|
|
multi-service stack with ingress configured:
|
|
- `web-app.md` — frontend Service (socket bind, PathPrefix route)
|
|
- `api.md` — backend API Service (TCP opt-in, `127.0.0.1` bind)
|
|
- `examples/full-stack/rendered/traefik-dynamic-web-app.yaml` — the
|
|
Traefik config Orca generates
|
|
|
|
## v0.11 forward (limitations)
|
|
|
|
The following are not yet implemented in v0.9 and will land in v0.11:
|
|
|
|
- **`service.host` / `service.route_id`**: stored on the `ServiceBlock`
|
|
but not yet consumed by the `TraefikEmitter`. The router rule is
|
|
hardcoded `PathPrefix("/<name>")`. Custom host-based routing lands in
|
|
v0.11.
|
|
- **Socket activation**: real socket-activation (socket unit files, fd
|
|
passing) lands in v0.11-P08. The current emitter renders the
|
|
`RuntimeDirectory` + socket path comments but does not create socket
|
|
units.
|
|
- **Transactional update execution**: the `update:` block's rolling/
|
|
canary/blue-green plan is computed by the emitter but not yet
|
|
executed transactionally. Transactional execution lands in
|
|
v0.11-P10.
|
|
- **SPIFFE SVID minting**: workload identity via step-ca SVIDs lands in
|
|
v0.11-P01.5 (gate C-08).
|
|
- **Secrets in env**: `env: { KEY: { from: "secret:..." } }` resolution
|
|
to `EnvironmentFile=`/`LoadCredential=` lands in v0.11-P03.
|
|
|
|
## See also
|
|
|
|
- [docs/cli.md](cli.md) — CLI reference
|
|
- [docs/jobspec.md](jobspec.md) — Jobspec reference (`service:`, `health:`, `ports:` blocks)
|
|
- [examples/full-stack/](../examples/full-stack/) — Full-stack example with ingress |