diff --git a/Security.md b/Security.md new file mode 100644 index 0000000..73a8e0c --- /dev/null +++ b/Security.md @@ -0,0 +1,86 @@ +# Security modes + +aptly itself has **no** authentication — upstream explicitly warns against putting +the REST API directly on the internet. This repo solves that with an nginx sidecar +(Compose: its own container; Helm: a second container in the same pod, `aptly` only +listens on `127.0.0.1`). + +## The four presets + +| `security.preset` | Read (apt clients) | Read auth | Write (`/api/`) | Write auth | +|---|---|---|---|---| +| `open` | on | **no** | on | **no** | +| `publicRead` (**default**) | on | no | on | yes | +| `authenticated` | on | yes | on | yes | +| `readOnly` | on | no | **off (404)** | — | + +`open` is the "completely unsecured" option you asked for — **one line**, no +confirmation gate: + +```yaml +security: + preset: open +``` + +Health probes (`/api/ready`, `/api/healthy`) are reachable without credentials in +**every** mode — otherwise Kubernetes/Compose could never recognize the instance as +healthy. + +## Every cell individually reachable + +`security.read.*` and `security.write.*` explicitly override the preset: + +```yaml +security: + preset: authenticated + read: + requireAuth: false # = publicRead, just spelled out explicitly +``` + +## The Ingress split (Helm) — what it actually does + +```yaml +ingress: + mode: split + api: + enabled: true + host: aptly-api.example.com +``` + +This creates two `Ingress` objects (different host, different +annotations/TLS/ingressClass possible) — **but both point at the same Service**. +nginx itself does not check which host a request arrived on; `/api/` is reachable +through **both** hosts, as long as `security.write.enabled` allows it. The split is +therefore a pure Ingress/DNS feature (e.g. a separate certificate or a WAF only on +the API host), not a hard network separation. For real isolation: +`security.write.inClusterOnly: true` (then renders no API Ingress at all — an honest +implementation, not an nginx trick) or a `networkPolicy`. + +## The CIDR pitfall + +```yaml +security: + write: + allowCIDRs: ["10.42.0.0/16"] +``` + +Behind an Ingress controller, `$remote_addr` in nginx is the **controller's pod IP**, +not the real client — `allowCIDRs` without `trustedProxies` then matches practically +everyone. Set `security.trustedProxies` to your Ingress controller's CIDR (enables +`X-Forwarded-For` evaluation), or use a `networkPolicy` for real L3 restriction. If +`trustedProxies` is missing while `allowCIDRs` is set, the chart renders a +`# WARNING` line directly into the nginx config. + +## Unsigned + +```yaml +aptly: + gpg: + enabled: false +``` + +Sets `gpg_disable_sign: true` in the aptly config **and** `Signing.Skip: true` in +every publish call the reconcile job makes — both are needed; aptly's publish API +takes its own signing parameter per call and ignores the global config flag +(empirically verified). The client then needs `[trusted=yes]` instead of +`signed-by=`.