diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-github-app.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-github-app.tsx index 92311b00b0..2daeea5ea8 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-github-app.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-github-app.tsx @@ -76,6 +76,18 @@ SELFHOST_SETUP_TOKEN=change-this-long-random-value # unlocks /setup for a fresh the token in an x-setup-token header or Authorization: Bearer header instead; never place the setup token in the URL.

+ + https://reviews.example.com above is a placeholder — it assumes you already + have a real domain terminating TLS. GitHub delivers webhooks to whatever{" "} + PUBLIC_API_ORIGIN you set here, so it must be an address GitHub's servers can + actually reach: the caddy profile (see{" "} + Security's TLS termination section) is the + shipped way to get one, or bring your own public reverse proxy. The tailscale{" "} + profile's private tailnet address does not work here — GitHub cannot + deliver webhooks to it. A Tailscale-only instance should use brokered pull mode instead (it + polls for work rather than receiving pushed webhooks) — see "Pull vs. push relay mode" + below. + Manual App creation (below) is still fully supported — for an air-gapped instance, a stricter change-review process, or simply a preference for reviewing every permission by @@ -243,7 +255,11 @@ ORB_RELAY_MODE=pull # or omit for push (the default) -- see "Choosing a relay m outage more gracefully (see the release checklist's known-warnings table below). Use push only once you already have a stable, publicly reachable HTTPS origin for this instance — the Direct App setup wizard, for instance, always requires one anyway, so an operator running - Direct App today has it available for brokered push mode too. + Direct App today has it available for brokered push mode too. See{" "} + Security's TLS termination section for how to + stand one up: the caddy profile for a public domain, or note that{" "} + tailscale's private tailnet address does not satisfy push mode's + internet-reachable requirement — pull mode is the right fit for a Tailscale-only instance. Before enabling this for anyone outside a controlled managed-beta cohort, weigh: (1){" "} @@ -293,15 +309,28 @@ ORB_RELAY_MODE=pull # or omit for push (the default) -- see "Choosing a relay m scenario for the smoke tests that exercise both relay modes.

-

Webhook checks

+

Connectivity checks

+

+ Confirm you can reach the instance at all before checking GitHub's own webhook delivery: +

+

+ reviews.example.com here stands in for whatever you're checking from — the{" "} + caddy profile's domain, an existing reverse proxy, or (if you're on the same + tailnet) a Tailscale instance's tailnet address on port 8787. This only confirms{" "} + you can reach the instance, not that GitHub can — a Tailscale-only + instance in push mode will pass this check and still never receive a real webhook, since + GitHub itself cannot reach a private tailnet address (see the callout above on{" "} + PUBLIC_API_ORIGIN). +

After installing the App on a test repo, open a small PR and confirm the webhook delivery - appears in GitHub and a job appears in self-host logs. Continue with{" "} + appears in GitHub and a job appears in self-host logs — this is the check that actually + proves GitHub can reach you. Continue with{" "} Operations for log and metric checks.

diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-quickstart.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-quickstart.tsx index 8c72690829..de8a35485e 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-quickstart.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-quickstart.tsx @@ -213,6 +213,13 @@ OPT-IN, NOT REQUIRED FOR A TRIAL INSTANCE contract, not a flag you turn on. The one way to disable it is the explicit air-gap flag: set ORB_AIR_GAP=true for an instance that sends nothing.
+

+ --profile caddy gets you real public HTTPS; --profile tailscale{" "} + adds private tailnet reachability (it does not remove the default public port on its own — + see the callout below) — see Security's TLS + termination section for the full walkthrough of each (Caddyfile setup, DNS prerequisites, + hardening Tailscale for real isolation, and when to pick one over the other). +

); } diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-security.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-security.tsx index 60e9f87854..f5ba5abfa6 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-security.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-security.tsx @@ -62,13 +62,39 @@ function SelfHostingSecurity() {

Network exposure

+

+ The observability profile also runs a docker-proxy service that + never appears in any dashboard or metric. It fronts the Docker socket for Promtail's + container log discovery: a plain :ro bind-mount of{" "} + /var/run/docker.sock only protects the socket inode, not the Docker API behind + it, so handing Promtail the raw socket is effectively host root — enumerate every container, + read each one's environment and secrets, tail every log, or start a privileged container and + escape to the host. docker-proxy is the only container that touches the socket, + exposes just the read-only /containers/* and /networks/* endpoints + Promtail's service discovery needs, denies every mutating call outright, and sits alone on + its own Docker network shared only with Promtail — publishing no host port isn't enough on + its own, since the default compose network is reachable by every other service in the stack. +

Control-panel access

@@ -103,6 +129,161 @@ function SelfHostingSecurity() { untrusted advisory context.

+

TLS termination

+

+ These are the three shipped ways to get real HTTPS without hand-rolling a reverse proxy — + but only Caddy and bring-your-own-proxy give you a publicly reachable origin. If + GitHub itself needs to reach this instance (a direct App in push mode, per{" "} + GitHub App and Orb), Tailscale's private + tailnet address does not satisfy that — GitHub's servers can't reach it. Tailscale is the + right fit when only your own team/CI needs access, or as the transport for a{" "} + brokered, pull-mode instance that never + needs to receive an inbound webhook at all. +

+ + +

Caddy: automatic HTTPS with Let's Encrypt

+

+ The caddy profile runs Caddy 2 in front of the gittensory service, + terminating TLS on 80/443/443/udp (the last for + HTTP/3) and obtaining a Let's Encrypt certificate automatically for whatever domain you set. + It needs a real DNS record: point DOMAIN at this host's public IP{" "} + before starting the profile. The shipped Caddyfile has no fallback TLS directive, + so if the ACME HTTP-01 challenge fails (DNS not propagated yet, port 80 unreachable), Caddy + does not silently substitute a self-signed cert for a real domain — it logs + the failure and retries with backoff, and the site has no working HTTPS until DNS and ACME + both succeed. (A recognized non-public hostname like localhost, below, is a + deliberately different case — Caddy issues its own internal-CA cert for those automatically, + since it can never get a real one.) +

+ +

+ The shipped caddy/Caddyfile reverse-proxies to gittensory:8787 on + the compose network, forwards the real client IP, enables compression, sets standard + security headers (HSTS, X-Content-Type-Options, X-Frame-Options, a + strict referrer policy), and logs as JSON to stderr: +

+ +

+ Edit this file directly if you need a different upstream, extra headers, or a second site + block — Caddy re-reads it on container restart. For local testing without a real domain, set{" "} + DOMAIN=localhost; Caddy issues a self-signed cert and your browser will warn + about it, which is expected. +

+ + The gittensory service's compose entry has a direct{" "} + {`ports: ["\${PORT:-8787}:8787"]`} mapping with a comment marking exactly this: + remove it once Caddy is your public listener, or the app stays reachable on{" "} + :8787 with no TLS, bypassing the proxy entirely and defeating the whole point + of adding it. (This rule is Caddy-specific — the Tailscale profile below needs the{" "} + opposite treatment; see its own callout.) + +

+ Prefer certificates you already manage — an internal CA, a wildcard cert issued elsewhere — + instead of Let's Encrypt? Mount your own cert and key into the container and point the{" "} + {`{$DOMAIN}`} block at a file-based TLS directive ( + tls /path/to/cert /path/to/key) instead of the automatic-HTTPS default; see{" "} + + Caddy's tls directive docs + {" "} + for the syntax. +

+ +

Already run a reverse proxy or load balancer?

+

+ Skip the caddy profile entirely. Remove the same direct ports:{" "} + mapping from the gittensory service, keep it on the compose network (or publish{" "} + 8787 bound to a private interface your existing proxy can reach), and terminate + TLS the way you already do for everything else — nginx, Traefik, an AWS ALB, a Cloudflare + Tunnel. Whatever fronts it just needs to forward to port 8787 and preserve the + client IP the same way the shipped Caddyfile does. +

+ +

Tailscale: adds tailnet reachability

+

+ The tailscale profile joins the stack to your tailnet. It runs with{" "} + network_mode: host — Tailscale needs host networking to advertise this + machine's address on the tailnet. On its own, this only adds a reachable address; + see the callout below before assuming it also removes public reachability. +

+ + + Tailscale doesn't replace the gittensory service's listener the way Caddy does + — it adds a new network interface to the host. Docker's default{" "} + {`ports: ["\${PORT:-8787}:8787"]`} mapping publishes to all of the host's + interfaces, so once Tailscale is up, that same mapping is what makes port 8787{" "} + reachable at the host's tailnet IP too —{" "} + + removing it, as you would for Caddy, makes the app unreachable everywhere, tailnet + included. + + +

+ The tradeoff: leaving the default 0.0.0.0-bound mapping in place means{" "} + 8787 is also still reachable from your LAN, and from the public internet if + this host has a public interface at all — Tailscale doesn't narrow that on its own. If you + want the instance reachable only via the tailnet, either firewall the host to allow{" "} + 8787 solely from your tailnet's address range, or bind the app's mapping to{" "} + 127.0.0.1:8787:8787 and use{" "} + + tailscale serve + {" "} + inside the tailscale container (it shares the host's loopback under{" "} + network_mode: host) to proxy that localhost-only port onto the tailnet — check + the pinned image's tailscale serve --help for the exact current flags. This + profile is the right choice when the instance only needs to be reachable by your own team or + CI, and you'd rather not manage a domain or certificate at all. +

+

Public output boundary

Public PR comments and checks must not leak secrets, private policy, provider credentials,