Blocked by #142
Context
With the cockpit loop-managed (#142), consumer projects need a self-serve way to expose it from the host machine to the internet, protected by Cloudflare Access. Requirement: independent per project — each consumer repo (reDeploy, reDeFi, …) gets its own named tunnel, credentials, config, and unit, so projects can be onboarded and torn down without touching each other. No shared machine-level ingress file, and nothing user-specific (domain, hostname, port) baked into plugin code.
Reference implementation to generalize: reDeploy's always-on studio tunnel (deploy/always-on/ + docs/ALWAYS-ON-TUNNEL.md in that repo) — named tunnel, per-tunnel config yml passed via --config (avoids the default-config.yaml wrong-tunnel hazard), systemd user unit, Access configured per hostname.
Security constraint (drives the workflow ordering): the cockpit has no authentication and its worker-inspector/SSE routes expose repo and git forensics. Cloudflare Access is the only gate, so DNS must never be routed before the Access application exists.
Proposal
- Extend the
gates.json cockpit block with expose: { hostname, tunnelName } (default: absent).
- New
cockpit-expose skill/script that, idempotently:
- checks
cloudflared is installed and ~/.cloudflared/cert.pem exists (else instructs cloudflared tunnel login);
- creates the per-project named tunnel (default name
<project>-cockpit);
- writes
~/.cloudflared/<project>-cockpit.yml with a single ingress rule hostname → http://localhost:<cockpit.port> plus the required http_status:404 catch-all;
- gates the DNS step on Access: walks the user through creating the Access self-hosted app in the Zero Trust dashboard and requires confirmation — or creates it via the API when a
CLOUDFLARE_API_TOKEN with Access:Apps edit scope is provided — before running cloudflared tunnel route dns --overwrite-dns <tunnel-uuid> <hostname>;
- wires the per-project cloudflared unit loop-scoped by default (started on arm / stopped on halt alongside the cockpit unit, addressing the tunnel by UUID and explicit
--config), with an always-on systemd option;
- provides a matching teardown path (unit, DNS route, tunnel, config).
- Offer the skill from
/orchestrator:setup when the user opts into cockpit exposure.
Acceptance criteria
- Running the skill in a consumer repo with a Cloudflare-managed zone yields a working
https://<hostname> cockpit behind Access, up while the loop is armed.
- DNS is provably never routed before the Access app is confirmed/created.
- Two projects on one machine expose independently: separate tunnels, configs, units; tearing one down leaves the other running.
- Re-running the skill is a no-op on an already-exposed project; teardown removes everything it created.
- Repos without
expose config (or without Cloudflare) are completely unaffected.
🤖 Generated with Claude Code
Blocked by #142
Context
With the cockpit loop-managed (#142), consumer projects need a self-serve way to expose it from the host machine to the internet, protected by Cloudflare Access. Requirement: independent per project — each consumer repo (reDeploy, reDeFi, …) gets its own named tunnel, credentials, config, and unit, so projects can be onboarded and torn down without touching each other. No shared machine-level ingress file, and nothing user-specific (domain, hostname, port) baked into plugin code.
Reference implementation to generalize: reDeploy's always-on studio tunnel (
deploy/always-on/+docs/ALWAYS-ON-TUNNEL.mdin that repo) — named tunnel, per-tunnel config yml passed via--config(avoids the default-config.yamlwrong-tunnel hazard), systemd user unit, Access configured per hostname.Security constraint (drives the workflow ordering): the cockpit has no authentication and its worker-inspector/SSE routes expose repo and git forensics. Cloudflare Access is the only gate, so DNS must never be routed before the Access application exists.
Proposal
gates.jsoncockpit block withexpose: { hostname, tunnelName }(default: absent).cockpit-exposeskill/script that, idempotently:cloudflaredis installed and~/.cloudflared/cert.pemexists (else instructscloudflared tunnel login);<project>-cockpit);~/.cloudflared/<project>-cockpit.ymlwith a single ingress rulehostname → http://localhost:<cockpit.port>plus the requiredhttp_status:404catch-all;CLOUDFLARE_API_TOKENwith Access:Apps edit scope is provided — before runningcloudflared tunnel route dns --overwrite-dns <tunnel-uuid> <hostname>;--config), with an always-on systemd option;/orchestrator:setupwhen the user opts into cockpit exposure.Acceptance criteria
https://<hostname>cockpit behind Access, up while the loop is armed.exposeconfig (or without Cloudflare) are completely unaffected.🤖 Generated with Claude Code