docs(pangolin): add apps/pangolin/README.md (DEV-461)
Documents the Pangolin + pangolin-kube-controller deployment: architecture, per-file component map, the two SSO flows (dashboard login vs downstream resource protection), the ingress-protection request flow, a step-by-step runbook for adding a new protected ingress, troubleshooting rooted in the DEV-457 findings, and hard "do not delete" notes on the two PVCs, the sealed secrets, and the Traefik HelmChartConfig that Pangolin depends on. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
ee56681f86
commit
73430bf378
1 changed files with 240 additions and 0 deletions
240
apps/pangolin/README.md
Normal file
240
apps/pangolin/README.md
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# Pangolin
|
||||
|
||||
Pangolin is BasicStack's self-hosted reverse proxy and SSO gateway. It sits in front of
|
||||
selected cluster services (e.g. `paperclip.basicstack.de`) and forces every request through
|
||||
Pocket-ID before it reaches the backend. Pangolin does not terminate TLS or handle the
|
||||
raw HTTP request path itself — it publishes a Traefik dynamic config, and the
|
||||
`pangolin-kube-controller` translates that config into Traefik CRDs that our existing
|
||||
Traefik ingress controller enforces.
|
||||
|
||||
Public URL: <https://pangolin.basicstack.de> — App source: <https://github.com/fosrl/pangolin>
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser ──HTTPS──► Traefik (ns=kube-system)
|
||||
│
|
||||
│ IngressRoute + Middleware (badger) + TraefikService
|
||||
│ ▲ managed by pangolin-kube-controller (ns=pangolin)
|
||||
│ │
|
||||
│ │ polls /api/v1/traefik-config every ~15s
|
||||
│ │
|
||||
▼ │
|
||||
Pangolin API (ns=pangolin)
|
||||
├─ port 3000 external dashboard API (session auth, behind Ingress)
|
||||
├─ port 3001 internal traefik-config API (Bearer auth, cluster-internal only)
|
||||
└─ port 3002 UI HTTP (behind Ingress)
|
||||
│
|
||||
├── PostgreSQL (ns=pangolin, pangolin-postgres)
|
||||
└── OIDC provider: Pocket-ID at https://auth.basicstack.de
|
||||
|
||||
Auth flow for a protected resource (e.g. paperclip.basicstack.de):
|
||||
Browser ──► Traefik ──► badger middleware ──callback──► Pangolin API (3001)
|
||||
│
|
||||
▼
|
||||
Pocket-ID / session
|
||||
```
|
||||
|
||||
Two long-running workloads live in the `pangolin` namespace:
|
||||
|
||||
- **Pangolin** — the app itself: dashboard, resource/target CRUD, OIDC handshake,
|
||||
session cookies, and the internal `/api/v1/traefik-config` endpoint that
|
||||
publishes the current desired Traefik state.
|
||||
- **pangolin-kube-controller** — a Kubernetes controller (leader-elected) that
|
||||
polls the Pangolin API, then upserts Traefik `IngressRoute`, `Middleware`,
|
||||
`TraefikService`, etc. via server-side apply into the `pangolin` namespace.
|
||||
|
||||
The controller only writes CRDs into `pangolin`; the backing Service typically
|
||||
lives in the target app's own namespace. Traefik is started with
|
||||
`--providers.kubernetescrd.allowCrossNamespace=true` so those cross-namespace
|
||||
refs resolve.
|
||||
|
||||
## Components (files in this directory)
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `namespace.yaml` | Creates the `pangolin` namespace. |
|
||||
| `pangolin-config.yaml` | Pangolin `config.yml` template (ConfigMap). `__DATABASE_URL__` is rendered at startup by the `render-config` init container from the sealed secret. |
|
||||
| `pangolin-deployment.yaml` | Pangolin app Deployment, Service (ports 3000/3001/3002), Ingress, and the `pangolin-data` PVC. |
|
||||
| `pangolin-secrets-sealed.yaml` | Sealed OIDC client id/secret and local admin password. |
|
||||
| `pangolin-postgres-secrets-sealed.yaml` | Sealed Postgres credentials plus the rendered `DATABASE_URL`. |
|
||||
| `postgres.yaml` | Single-replica Postgres 17.5 + `pangolin-postgres-data` PVC. |
|
||||
| `pangolin-controller-config.yaml` | `pangolin-kube-controller` env: `CONFIG_ENDPOINT`, target namespace, log level. |
|
||||
| `pangolin-controller-deployment.yaml` | Controller Deployment (image `fosrl/pangolin-kube-controller:0.1.0-alpha.1`). |
|
||||
| `pangolin-controller-rbac.yaml` | ServiceAccount + ClusterRole/Binding: CRUD on Traefik CRDs and leases. |
|
||||
| `pangolin-controller-service.yaml` | ClusterIP for controller metrics. |
|
||||
| `pangolin-controller-api-key-sealed.yaml` | Sealed `Authorization` header the controller sends to the internal API. |
|
||||
|
||||
ArgoCD Application: [`apps/app-pangolin.yaml`](../app-pangolin.yaml) — sync policy is
|
||||
`automated: { prune: true, selfHeal: true }`. Any local edit must land in git.
|
||||
|
||||
## SSO integration (Pocket-ID)
|
||||
|
||||
Pangolin authenticates users against Pocket-ID via OIDC. Two independent SSO
|
||||
flows exist and should not be confused:
|
||||
|
||||
1. **Logging into the Pangolin dashboard.** Configured in
|
||||
`pangolin-config.yaml` (`oidc.*`) and via env in `pangolin-deployment.yaml`:
|
||||
- Issuer: `https://auth.basicstack.de`
|
||||
- Callback URL: `https://pangolin.basicstack.de/auth/callback`
|
||||
- Scopes: `openid profile email groups`
|
||||
- Client id/secret: `pangolin-secrets` sealed secret keys
|
||||
`oidc-client-id` / `oidc-client-secret`.
|
||||
2. **Protecting a downstream resource (e.g. paperclip.basicstack.de).**
|
||||
Handled by the badger Traefik middleware that the controller emits.
|
||||
The middleware calls back into Pangolin's internal API (port 3001) to
|
||||
check the session cookie, and if unauthenticated redirects the browser
|
||||
through Pangolin's own OIDC flow. There is nothing to configure per-resource
|
||||
in Kubernetes — the resource's auth policy is set inside the Pangolin UI.
|
||||
|
||||
The Pocket-ID client is provisioned separately (see [pocket-id docs](https://pocket-id.org/docs/client-examples/pangolin)).
|
||||
Do not reconfigure the OIDC client from the Pangolin side once it is working.
|
||||
|
||||
## Ingress protection workflow
|
||||
|
||||
The end-to-end flow for a protected hostname like `paperclip.basicstack.de`:
|
||||
|
||||
1. Operator creates a **Resource** in the Pangolin UI (hostname, backend
|
||||
Service, port, auth policy).
|
||||
2. Pangolin persists it in Postgres and exposes the new desired state at
|
||||
`http://pangolin.pangolin.svc.cluster.local:3001/api/v1/traefik-config`.
|
||||
3. `pangolin-kube-controller` polls that endpoint on a ~15s loop.
|
||||
4. On change, the controller server-side-applies into `ns=pangolin`:
|
||||
- `IngressRoute` for the hostname (with `tls.certResolver: letsencrypt`).
|
||||
- `Middleware/badger` (session check + OIDC redirect).
|
||||
- `TraefikService/1-<resource>-service` pointing at the backend
|
||||
Service in the app's namespace, port from the DB.
|
||||
5. Traefik picks the new CRDs up and starts serving the route.
|
||||
6. First request → redirected to Pocket-ID → session cookie → subsequent
|
||||
requests are proxied to the backend Service.
|
||||
|
||||
## Adding a new protected ingress
|
||||
|
||||
Prereqs the target app must already satisfy:
|
||||
|
||||
- A Kubernetes `Service` in its own namespace, listening on a known port.
|
||||
- A DNS record for the hostname pointing at the cluster ingress IP.
|
||||
- No competing plain `Ingress` in another namespace for the same hostname.
|
||||
If one exists (e.g. a legacy direct ingress), delete it first — Traefik
|
||||
will otherwise route the pre-existing ingress and Pangolin's IngressRoute
|
||||
never wins.
|
||||
|
||||
Steps:
|
||||
|
||||
1. **Log in** to <https://pangolin.basicstack.de> (SSO via Pocket-ID, or
|
||||
local admin from the `pangolin-secrets` sealed secret).
|
||||
2. **Create the Resource.**
|
||||
- Type: HTTP/HTTPS.
|
||||
- Domain: the exact FQDN (e.g. `newapp.basicstack.de`).
|
||||
- Base domain: `basicstack.de`.
|
||||
3. **Target.**
|
||||
- Target type: HTTP.
|
||||
- Backend URL: `http://<service>.<namespace>.svc.cluster.local:<port>`
|
||||
— the port here is written to the `targets.port` column in Pangolin's
|
||||
Postgres and used by the controller when it generates the `TraefikService`.
|
||||
If the Service port later changes, update the target port in the UI (or
|
||||
via SQL: `UPDATE targets SET port=<N> WHERE "targetId"=<id>;`).
|
||||
- Path match: `/`, strip prefix off.
|
||||
4. **TLS.** Enable TLS, force HTTPS redirect. The Pangolin-generated IngressRoute
|
||||
hardcodes `tls.certResolver: letsencrypt`, which is provisioned on our Traefik
|
||||
install (HTTP-01, persistent `/data` PVC). Do not disable that resolver in the
|
||||
Traefik `HelmChartConfig` without also removing every Pangolin-generated route
|
||||
— Traefik will otherwise fail with `nonexistent certificate resolver`.
|
||||
5. **Auth.** Enable authentication, method SSO/OIDC. In Community Edition (what we
|
||||
run) there are no shared policies or group rules — add authorised users
|
||||
individually by email. If you need group-based rules, that's an Enterprise
|
||||
feature.
|
||||
6. **Save** — controller applies CRDs within a poll interval (~15s).
|
||||
7. **Verify.**
|
||||
|
||||
```bash
|
||||
kubectl -n pangolin get ingressroute,middleware,traefikservice \
|
||||
| grep -i <resource-name>
|
||||
kubectl -n pangolin logs -l app=pangolin-controller --tail=50
|
||||
# HTTP smoke test
|
||||
curl -sSI https://<host>/ # expect 302 to Pocket-ID
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **badger middleware returns HTTP 500.** The middleware is trying to call
|
||||
`apiBaseUrl` and either the URL is not an FQDN or Pangolin isn't reachable
|
||||
from `kube-system`. Confirm `pangolin-config.yaml` sets
|
||||
`server.internal_hostname` and `server.badger_override` to
|
||||
`pangolin.pangolin.svc.cluster.local` — the short name `pangolin` does not
|
||||
resolve from Traefik's namespace.
|
||||
- **Controller logs `conflict with "controller" using traefik.io/v1alpha1: .spec…`.**
|
||||
A pre-existing CRD was created with the `Update` verb and the current
|
||||
controller uses server-side `Apply`. Delete the offending
|
||||
`Middleware/TraefikService/IngressRoute` once — the controller recreates it
|
||||
via SSA and the conflict is gone.
|
||||
- **Traefik logs `service port not found: <N>`.** The port stored in Pangolin's
|
||||
`targets` table doesn't match the Kubernetes Service port. Fix in the UI, or:
|
||||
`UPDATE targets SET port=<N> WHERE "targetId"=<id>;` (identifier case matters,
|
||||
double-quote it).
|
||||
- **Traefik logs `nonexistent certificate resolver: letsencrypt`.** The
|
||||
`letsencrypt` resolver isn't loaded in Traefik. Every Pangolin IngressRoute
|
||||
references it — either define the resolver in Traefik (see
|
||||
`infrastructure/networking/traefik-helmchartconfig.yaml`) or override
|
||||
`traefik.cert_resolver` in `pangolin-config.yaml`. Grep the Traefik startup
|
||||
log for `unable to get ACME account`; that appears when the resolver was
|
||||
skipped entirely.
|
||||
- **Route doesn't resolve cross-namespace.** Traefik must run with
|
||||
`--providers.kubernetescrd.allowCrossNamespace=true`. Pangolin's CRDs live in
|
||||
`pangolin`; backend Services usually don't.
|
||||
- **CRDs never appear after saving a Resource.**
|
||||
```bash
|
||||
kubectl -n pangolin get pods -l app=pangolin-controller
|
||||
kubectl -n pangolin logs -l app=pangolin-controller --tail=200
|
||||
kubectl -n pangolin get configmap pangolin-controller-config -o yaml
|
||||
```
|
||||
Common causes: controller CrashLoop, wrong `CONFIG_ENDPOINT`, missing auth
|
||||
header sealed secret, RBAC missing after a controller image upgrade.
|
||||
- **Login loop / redirects to Pocket-ID forever.** Clear cookies for both
|
||||
`pangolin.basicstack.de` and `auth.basicstack.de`, confirm the OIDC callback
|
||||
URL matches the Pocket-ID client, and check Pangolin logs for OIDC errors:
|
||||
`kubectl -n pangolin logs -l app=pangolin`.
|
||||
- **Pangolin pod stuck in `Init:0/2`.** The `wait-for-postgres` init container
|
||||
is looping on `pg_isready`. Check `pangolin-postgres` health and the
|
||||
`pangolin-postgres-secrets` connection details.
|
||||
|
||||
## Critical notes — DO NOT DELETE
|
||||
|
||||
Pangolin holds live user/account/config state. Losing a PVC here means
|
||||
re-provisioning every protected resource, every target port, every user grant,
|
||||
and re-doing the OIDC client trust from scratch — plus temporary outages on
|
||||
every downstream site that Pangolin fronts.
|
||||
|
||||
- **`pangolin-postgres-data` PVC (ns=pangolin, 10 Gi, `hcloud-volumes-encrypted`).**
|
||||
Backs Postgres. Contains resources, targets, users, sessions, and everything
|
||||
the controller reads. Annotated with
|
||||
`argocd.argoproj.io/sync-options: Delete=false` — do not remove that
|
||||
annotation, and do not delete the PVC as a "reset" step. Snapshot the volume
|
||||
before any migration or destructive DB operation.
|
||||
- **`pangolin-data` PVC (ns=pangolin, 10 Gi, `hcloud-volumes-encrypted`).**
|
||||
Runtime files under `/app/config`. Also marked `Delete=false`. Do not delete.
|
||||
- **Sealed secrets** (`pangolin-secrets`, `pangolin-postgres-secrets`,
|
||||
`pangolin-controller-api-key`) — rotating them out of order will lock the
|
||||
controller out of the internal API or take Pangolin off Postgres. Rotate one
|
||||
secret at a time and verify the corresponding pod comes back before
|
||||
proceeding.
|
||||
- **`pangolin-config` ConfigMap.** Editing `server.internal_hostname` /
|
||||
`server.badger_override` back to the default short hostname will break the
|
||||
badger middleware cluster-wide (see Troubleshooting).
|
||||
- **The Traefik `HelmChartConfig`** in `kube-system`
|
||||
(`infrastructure/networking/traefik-helmchartconfig.yaml`) — Pangolin depends
|
||||
on `allowCrossNamespace=true` and on the `letsencrypt` resolver being loaded
|
||||
with a persistent `/data` PVC. Removing either silently breaks every
|
||||
Pangolin-managed route.
|
||||
|
||||
If access to Pangolin is broken, **restore from a snapshot first; do not
|
||||
"reset" or re-provision** the admin/OIDC layer. That is the standing rule for
|
||||
every stateful service in this cluster.
|
||||
|
||||
## Related
|
||||
|
||||
- History and gotchas: internal memory `pangolin-kube-integration` (from DEV-457).
|
||||
- Configuration walkthrough for the `paperclip.basicstack.de` resource:
|
||||
[`pangolin-paperclip-configuration-guide-community.md`](pangolin-paperclip-configuration-guide-community.md).
|
||||
- Pangolin docs: <https://docs.pangolin.net/>
|
||||
- pangolin-kube-controller: <https://github.com/fosrl/pangolin-kube-controller>
|
||||
Loading…
Add table
Reference in a new issue