feat(paperclip): LAN-only API door for external agents #1029

Merged
k.renner merged 1 commits from feat/paperclip-api-door into master 2026-09-26 09:24:21 +00:00
Member

Lets agents running outside the pod (Claude Code on the workstation) work as Paperclip company agents.

External agents only make outbound REST calls to /api/* with Authorization: Bearer <agent key>; the Authentik gate on paperclip.renner.uno rejects those. Machine door, per the wiki-index pattern:

  • paperclip-api.renner.uno — LAN-only (no dns.renner.uno/public label), routes /api straight to the paperclip Service. Paperclip's bearer keys are the auth.
  • /api/auth on that host → 403 (Traefik ipAllowList matching nothing), so password sign-in/sign-up stays behind the gate (upstream serves all password login under /api/auth).
  • Host added to PAPERCLIP_ALLOWED_HOSTNAMES; CNP admits Traefik on 3100.

Trade-off: anything on the LAN can reach /api; only Paperclip's key auth protects it.

After merge: mint an agent key (paperclipai agent local-cli <name>) and use PAPERCLIP_API_URL=https://paperclip-api.renner.uno.

🤖 Generated with Claude Code

Lets agents running outside the pod (Claude Code on the workstation) work as Paperclip company agents. External agents only make outbound REST calls to `/api/*` with `Authorization: Bearer <agent key>`; the Authentik gate on `paperclip.renner.uno` rejects those. Machine door, per the wiki-index pattern: - `paperclip-api.renner.uno` — **LAN-only** (no `dns.renner.uno/public` label), routes `/api` straight to the `paperclip` Service. Paperclip's bearer keys are the auth. - `/api/auth` on that host → 403 (Traefik `ipAllowList` matching nothing), so password sign-in/sign-up stays behind the gate (upstream serves all password login under `/api/auth`). - Host added to `PAPERCLIP_ALLOWED_HOSTNAMES`; CNP admits Traefik on 3100. Trade-off: anything on the LAN can reach `/api`; only Paperclip's key auth protects it. After merge: mint an agent key (`paperclipai agent local-cli <name>`) and use `PAPERCLIP_API_URL=https://paperclip-api.renner.uno`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot added 1 commit 2026-09-26 09:21:59 +00:00
feat(paperclip): LAN-only API door for external agents
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
053e0f1a9b
Agents outside the pod (Claude Code on the workstation, via
`paperclipai agent local-cli` / agent API keys) only make outbound REST
calls to /api/* with Authorization: Bearer. The Authentik gate on
paperclip.renner.uno rejects those (intercept_header_auth), so they get a
separate machine door, following the wiki-index pattern:

- paperclip-api.renner.uno (no dns.renner.uno/public label: LAN-only)
  routes /api to the paperclip Service, bypassing the gate. Paperclip's
  bearer agent/board keys are the authentication.
- /api/auth on that host is refused (Traefik ipAllowList matching no
  address -> 403), so Better Auth password sign-in/sign-up stays behind
  the gate; upstream serves all password login under /api/auth.
- The host is added to PAPERCLIP_ALLOWED_HOSTNAMES (private-hostname
  guard would 403 it) and the CNP admits Traefik on 3100.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
k.renner merged commit 783a680184 into master 2026-09-26 09:24:21 +00:00
review-bot left a comment
Member

🤖 Automated review — model CLI default (complexity: complex), commit 053e0f1a9b478eeb9044db04f7e3b0b51269eaf8.

Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.


Summary

Adds a LAN-only Ingress (paperclip-api.renner.uno) that lets external agents hit Paperclip's /api directly with a bearer key, bypassing the Authentik gate, while a second Ingress + ipAllowList Middleware is meant to keep /api/auth (password sign-in/sign-up) blocked on that host. The manifests are internally consistent with the rest of the repo's conventions (Cilium CNP labels, cert-manager/secret reuse, dns.renner.uno/public opt-out), but the auth-block mechanism departs from this repo's own established pattern in a way worth a second look before merge.

No blocking issues found, but one point below is security-relevant enough that I'd want it confirmed before relying on it in production.

🟡 Suggestions

  • kubernetes/apps/paperclip/api-door.yaml:9-68 — the /api/auth block relies entirely on implicit Traefik router-priority ordering, with no fallback. The two Ingress objects for the same host (/api → real backend, /api/auth → deny-all middleware) rely on Traefik's Kubernetes-Ingress provider picking the longer/more-specific PathPrefix('/api/auth') rule over PathPrefix('/api') — no traefik.ingress.kubernetes.io/router.priority annotation is set to make that explicit. This is very likely correct (Traefik's default priority is derived from rule length), but it's the sole protection keeping password sign-in/sign-up off this LAN-reachable host — there's no secondary guard the way this repo has consistently built elsewhere:
    • kubernetes/apps/wiki/ingress.yaml sidesteps the ambiguity entirely by putting the machine door on a different hostname (wiki-index.renner.uno) than the gated one (wiki.renner.uno), so there's never a same-host path-priority question.
    • kubernetes/apps/paperless/docs-traefik-ingress.yaml:5-8 explicitly reasons about the failure mode ("even if the redirect regex misses, the outpost rejects... so there is no failure mode that leaks access") and designs so a miss is still safe.
      Here, if the priority ever resolved the other way (Traefik upgrade, provider quirk, a future PR adding another overlapping path under /api), requests to /api/auth would silently fall through to the real backend with no compensating control. I did not find anything indicating this is currently broken — it should work — but given this repo's own track record of building in a safety net for exactly this kind of ordering assumption, consider either an explicit router.priority annotation on paperclip-api-auth-deny (belt-and-suspenders, cheap) or moving the machine door to its own hostname as the wiki-index pattern does. Worth verifying post-deploy with a direct request to https://paperclip-api.renner.uno/api/auth/... and confirming a 403.

🟢 Nits

  • kubernetes/apps/paperclip/api-door.yaml:36-39 — the Middleware is named deny-all, which is generic for a namespace that may grow more middlewares later. Every other Middleware in this repo is named after its specific function/app (wiki-api-basic-auth, docs-to-paperless-redirect, loki-basic-auth). Something like paperclip-api-auth-deny (matching the Ingress it's paired with) would be more consistent and self-documenting in kubectl get middleware -A output.

Tests

This is a Kubernetes manifest change with no traditional test suite; correctness here means "does Traefik/Cilium actually enforce what the comments say" post-deploy. The one thing worth manually confirming after merge (noted above) is that GET/POST https://paperclip-api.renner.uno/api/auth/... returns 403 rather than reaching the backend, since that's the crux of the security boundary this PR introduces. The /api bearer-key path and the CiliumNetworkPolicy admission are lower-risk and match existing, already-proven patterns in this file.

Recommendation

Looks reasonable — no blocking issues; the suggestion above (explicit router priority or a separate machine-door hostname, plus a post-deploy check of the /api/auth block) is worth doing given it's a password-auth bypass boundary, but nothing here is a confirmed defect.

🤖 **Automated review** — model `CLI default` (complexity: complex), commit `053e0f1a9b478eeb9044db04f7e3b0b51269eaf8`. > Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review. --- ## Summary Adds a LAN-only Ingress (`paperclip-api.renner.uno`) that lets external agents hit Paperclip's `/api` directly with a bearer key, bypassing the Authentik gate, while a second Ingress + `ipAllowList` Middleware is meant to keep `/api/auth` (password sign-in/sign-up) blocked on that host. The manifests are internally consistent with the rest of the repo's conventions (Cilium CNP labels, cert-manager/secret reuse, `dns.renner.uno/public` opt-out), but the auth-block mechanism departs from this repo's own established pattern in a way worth a second look before merge. No blocking issues found, but one point below is security-relevant enough that I'd want it confirmed before relying on it in production. ## 🟡 Suggestions - **`kubernetes/apps/paperclip/api-door.yaml:9-68` — the `/api/auth` block relies entirely on implicit Traefik router-priority ordering, with no fallback.** The two Ingress objects for the same host (`/api` → real backend, `/api/auth` → `deny-all` middleware) rely on Traefik's Kubernetes-Ingress provider picking the longer/more-specific `PathPrefix('/api/auth')` rule over `PathPrefix('/api')` — no `traefik.ingress.kubernetes.io/router.priority` annotation is set to make that explicit. This is very likely correct (Traefik's default priority is derived from rule length), but it's the *sole* protection keeping password sign-in/sign-up off this LAN-reachable host — there's no secondary guard the way this repo has consistently built elsewhere: - `kubernetes/apps/wiki/ingress.yaml` sidesteps the ambiguity entirely by putting the machine door on a **different hostname** (`wiki-index.renner.uno`) than the gated one (`wiki.renner.uno`), so there's never a same-host path-priority question. - `kubernetes/apps/paperless/docs-traefik-ingress.yaml:5-8` explicitly reasons about the failure mode ("even if the redirect regex misses, the outpost rejects... so there is no failure mode that leaks access") and designs so a miss is still safe. Here, if the priority ever resolved the other way (Traefik upgrade, provider quirk, a future PR adding another overlapping path under `/api`), requests to `/api/auth` would silently fall through to the real backend with no compensating control. I did not find anything indicating this is currently broken — it should work — but given this repo's own track record of building in a safety net for exactly this kind of ordering assumption, consider either an explicit `router.priority` annotation on `paperclip-api-auth-deny` (belt-and-suspenders, cheap) or moving the machine door to its own hostname as the wiki-index pattern does. Worth verifying post-deploy with a direct request to `https://paperclip-api.renner.uno/api/auth/...` and confirming a 403. ## 🟢 Nits - `kubernetes/apps/paperclip/api-door.yaml:36-39` — the Middleware is named `deny-all`, which is generic for a namespace that may grow more middlewares later. Every other Middleware in this repo is named after its specific function/app (`wiki-api-basic-auth`, `docs-to-paperless-redirect`, `loki-basic-auth`). Something like `paperclip-api-auth-deny` (matching the Ingress it's paired with) would be more consistent and self-documenting in `kubectl get middleware -A` output. ## Tests This is a Kubernetes manifest change with no traditional test suite; correctness here means "does Traefik/Cilium actually enforce what the comments say" post-deploy. The one thing worth manually confirming after merge (noted above) is that `GET/POST https://paperclip-api.renner.uno/api/auth/...` returns 403 rather than reaching the backend, since that's the crux of the security boundary this PR introduces. The `/api` bearer-key path and the CiliumNetworkPolicy admission are lower-risk and match existing, already-proven patterns in this file. ## Recommendation Looks reasonable — no blocking issues; the suggestion above (explicit router priority or a separate machine-door hostname, plus a post-deploy check of the `/api/auth` block) is worth doing given it's a password-auth bypass boundary, but nothing here is a confirmed defect. <!-- pr-review:sha=053e0f1a9b478eeb9044db04f7e3b0b51269eaf8 -->
Sign in to join this conversation.