- Python 79.8%
- CUE 11.1%
- Just 9.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WTenpGsjD8NN1NeVyzAcg8 |
||
| quality | ||
| tests | ||
| .gitignore | ||
| .woodpecker.yml | ||
| app.py | ||
| justfile | ||
| README.md | ||
hello-api
Reference API for the reti platform, live at https://api.reti-infra.com. One JSON endpoint, three authentication paths, all against the platform's own identity stack (Kanidm on kanidm-01). This repo is the template for the 5–10 production APIs that follow.
Three ways in
| Caller | Path | Mechanism |
|---|---|---|
| Human | browser → /login |
Kanidm OIDC authorization-code + PKCE (passkey). Group hello_api_users required. |
| AI agent (token) | Authorization: Bearer … |
Kanidm RFC 8693 token exchange: service-account API token → short-lived (900 s) OAuth2 access token, audience=hello-api. Validated server-side by RFC 7662 introspection. Group hello_api_agents. |
| AI agent (PKI) | TLS client certificate | mTLS at Caddy against the reti agent CA (/etc/reti/agent-ca/ca.crt on apps-01). Identity = certificate CN. Certificate possession is the authorisation. |
Both agent paths coexist with the human path on the same vhost: Caddy's
client_auth mode verify_if_given lets browsers connect certificate-less
while any presented certificate must chain to the agent CA.
Agent quickstart (token exchange)
TOK=<service-account api token> # kanidm service-account api-token generate svc-hello-agent …
AT=$(curl -s -X POST https://idm.reti-infra.com/oauth2/token \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d client_id=hello-api -d audience=hello-api \
--data-urlencode subject_token="$TOK" \
-d subject_token_type=urn:ietf:params:oauth:token-type:access_token \
-d "scope=openid groups_name" | jq -r .access_token)
curl -H "Authorization: Bearer $AT" https://api.reti-infra.com/api/hello
Note: for Kanidm basic OAuth2 clients the API token itself is the credential in this flow — do not send the client secret.
Agent quickstart (PKI / mTLS)
curl --cert svc-hello-agent.crt --key svc-hello-agent.key \
https://api.reti-infra.com/api/hello
Certificates are issued from the agent CA on apps-01
(/etc/reti/agent-ca/, key root-only). Issue a new agent cert with openssl:
CSR with CN=<agent name>, sign with the CA, extendedKeyUsage=clientAuth.
Where the new APIs should live (platform decision)
- Host: apps-01, loopback listener (ports 5000–5999, one per service), behind Caddy. No new hosts needed at this scale; the CVM has headroom.
- Routing: one subdomain vhost per API (
<name>.reti-infra.com) — matches every existing service on the box, gives per-API TLS/mTLS policy, and avoids path-prefix rewriting. Reserveapi.reti-infra.comfor this reference/gateway; a future consolidated gateway can grow here. - Process model: one systemd unit per API,
User=www-data(or a dedicated user), secrets viaLoadCredential=from/etc/reti/secrets/(root-only), non-secret config in/opt/<name>/<name>.env(644). Containers (podman quadlet) are the alternative for non-Python runtimes — both patterns are already in production on the host. - Source of truth: one Forgejo repo per API (this repo is the template),
CI in Woodpecker, images (if any) in zot (
registry.reti-infra.com).
Dev workflow (Woodpecker + CUE)
- Branch, edit, push to Forgejo → webhook fires Woodpecker
(
ci.reti-infra.com, agent on apps-01, podman backend). .woodpecker.ymlruns two mandatory gates:- quality-gate —
cue vet ./quality/:quality/release.cue(the service's concrete manifest: port, vhost, groups, credential names, UTC declaration) must satisfy the platform schemaquality/service.cue. Policy lives in the schema; violating manifests cannot merge. - test —
python3 -m py_compile+ stdlib unit tests.
- quality-gate —
- Merge to
mainon green. - Deploy (operator or agent, from the cockpit):
just deploy— fetchesapp.pyfrom Forgejo raw, installs to/opt/hello-api/, restarts the unit, probes/healthz.
One-time per new repo: an operator activates the repo in the Woodpecker UI (https://ci.reti-infra.com, Kanidm-federated via Forgejo login) so the webhook is installed.
The CUE schema is deliberately platform-wide: copy quality/service.cue
into each new API repo (or later, publish it as a CUE module in Forgejo) and
write a ten-line release.cue. The gate then enforces the hosting decisions
above mechanically.
Identity: what Kanidm can and cannot do (researched 2026-08-13)
- PKI certificates for users/agents inside Kanidm: not supported.
Kanidm has no x509 login flow. That is why PKI lives at the TLS edge
(Caddy
client_auth) with our own CA. This works today and is what this service demonstrates. - Machine credentials: Kanidm has no OAuth2
client_credentialsgrant (issue #2217); the supported machine flow is service-account API tokens + RFC 8693 token exchange, which this service uses. - Google login: not possible through Kanidm. Kanidm has no upstream IdP federation — it cannot delegate authentication to Google, GitHub, or any external OIDC/SAML provider (issue #4015, discussion #3163). Federation is on the roadmap, not prioritised.
- GitHub login: same answer — not through Kanidm.
- Can PKI certs + Google (or GitHub) login coexist on one API? Yes —
they compose at different layers, so Kanidm's limitation only affects
where the human login comes from:
- mTLS stays at Caddy exactly as now (agents, certificate-holders);
- human OIDC can point at any OIDC provider. To offer
Google/GitHub/Kanidm side by side, put a small federating broker in
front: Dex (single binary, config-file driven,
fits the host's no-container-required doctrine) with three connectors —
oidc(Google),github, andoidc(Kanidm). Apps then speak OIDC to Dex only. Alternatives with the same shape: Keycloak, authentik, Zitadel (all heavier). A secondoauth2-proxyinstance per provider is the no-new-software fallback but cannot mix providers on one cookie. - Google side: create an OAuth client in Google Cloud Console
(redirect
https://<broker>/callback); GitHub side: an OAuth App in GitHub Developer settings. Their client ids/secrets go in the broker's config, delivered viaLoadCredential=like every other secret here.
Where this configuration is stored (file inventory)
| Concern | Location |
|---|---|
| Reverse proxy, vhosts, mTLS client_auth | /etc/caddy/Caddyfile (apps-01); agent CA trust copy /etc/caddy/agent-ca.crt |
| Agent PKI CA + issued certs | /etc/reti/agent-ca/ (apps-01, root-only key) |
| Kanidm server daemon | /etc/kanidmd/server.toml (kanidm-01); client defaults /etc/kanidm/config |
| Kanidm OAuth2 clients, groups, accounts, scope maps | not files — objects in the Kanidm database (/var/lib/kanidmd/kanidm.db), managed via kanidm system oauth2 …, kanidm group …, kanidm service-account … |
| This app's non-secret config | /opt/hello-api/hello-api.env (apps-01) |
| This app's secrets | /etc/reti/secrets/helloapi_* → systemd LoadCredential= in /etc/systemd/system/hello-api.service |
| oauth2-proxy pattern (existing ai-gateway) | /etc/oauth2-proxy/oauth2-proxy.cfg (apps-01) |
| A future Dex broker | /etc/dex/config.yaml (connectors incl. Google/GitHub client ids) + secrets via LoadCredential |
| Woodpecker CI | /etc/woodpecker/server.env, /etc/woodpecker/agent.env (apps-01) |
DNS (api.reti-infra.com → apps-01) |
Cloudflare zone reti-infra.com |
Kanidm objects backing this service
group hello_api_users (members: thomas.miller)
group hello_api_agents (members: svc-hello-agent, agent-claude)
svc acct svc-hello-agent (API token "hello-api-demo")
oauth2 hello-api origin https://api.reti-infra.com/
redirect https://api.reti-infra.com/callback
scope maps: users → openid profile email groups_name
agents → openid groups_name
Repo layout
app.py the service (Python stdlib only, ~250 lines)
quality/service.cue platform quality-gate schema (CUE)
quality/release.cue this service's concrete manifest
tests/test_app.py stdlib unit tests
.woodpecker.yml CI: cue vet + tests
justfile deploy/status/demo recipes (run from the cockpit)