feat(rhodibot): deploy behind a Cloudflare Tunnel, loopback origin - #541
Merged
Merged
Conversation
App authentication existed but nothing used it: GitHubClient still read a single GITHUB_TOKEN, so every repository request went out as one identity. Credentials are now resolved once, in the client constructor: - app_id + private key -> GitHub App. Each request is scoped to the repository it concerns, because installation tokens do not carry across repositories. - otherwise GITHUB_TOKEN, otherwise anonymous (unchanged behaviour). A half-configured App is refused rather than downgraded. Setting app_id with no key (or a key with no app_id) leaves the client "misconfigured": readiness() fails, so main.rs exits at start-up instead of serving traffic, and authorize() refuses to send the request at all. Quietly falling back to anonymous access would turn a config mistake into a permissions puzzle at the first webhook. /health now names the credential mode so an operator can see what was picked up. Two defects that only bite at fleet scale are fixed in AppAuth while wiring it: the token cache held one installation, so a sweep across owners re-minted a token on nearly every request; and the installation lookup - an API call billed to the App's own much smaller rate-limit budget - was repeated per request. Both are now keyed caches. file_exists reports a boolean by contract, so an authentication failure is logged and reported as absent rather than silently mass-reporting non-compliance. Tests: 5 new unit tests (100 total, 50 unit + 50 integration), including the handshake end to end against a mock server that answers only to the installation token, reuse across requests, and the two misconfiguration paths.
Adds the deployment the App credentials were built for: a tunnel config whose
ingress publishes POST /webhook and GET /health and 404s everything else, the
two systemd units, an environment-file template, an App-registration sheet, and
a verification script.
The origin now binds 127.0.0.1 by default instead of 0.0.0.0. A tunnel-fronted
service that also listens on every interface is published twice, and the second
publication has no webhook secret in front of it. `--bind 0.0.0.0` restores the
previous behaviour for anyone serving directly.
/api/check/{owner}/{repo} is deliberately not routed: it spends GitHub API quota
and reports on repositories, so it stays an operator endpoint.
deploy/verify-rhodibot.sh checks the deployment from the outside in -- origin
health and credential mode, that the port is loopback-only, that the tunnel
reaches it, that an unsigned delivery is refused with 401 (which also proves the
webhook secret is set: without one the handler accepts anything), and that the
private endpoints 404. PASS/FAIL/SKIP are distinguished, because a check that
could not be made is not a check that passed.
Contributor
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (8)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Depends on #539 — this branch is built on it, because the deployment assumes
the start-up credential gate that #539 adds. Merge that first; if it squashes,
this branch will need a rebase onto
main(the deploy files are all new, so itis mechanical).
The tunnel is the public origin; the host stays unreachable.
cloudflareddialsout to Cloudflare's edge and forwards back down that connection to a loopback
origin — no inbound port, no certificate on this host.
Loopback by default. The origin previously bound
0.0.0.0unconditionally.A tunnel-fronted service that also listens on every interface is published
twice, and the second publication has no webhook secret in front of it.
--bind 0.0.0.0/BIND_ADDR=0.0.0.0restores the old behaviour.Ingress publishes exactly two routes:
POST /webhookandGET /health.Everything else is
http_status:404, including any route added to the routerlater without a matching rule — the default is denial.
GET /api/check/{owner}/{repo}is deliberately not published: it spends GitHub API quota and reports on
repositories.
deploy/cloudflared/rhodibot.ymldeploy/systemd/rhodibot.serviceProtectHome, read-only/etcdeploy/systemd/cloudflared-rhodibot.serviceRequires=rhodibot.serviceso a tunnel is never up without an origin behind itdeploy/rhodibot.env.exampledeploy/GITHUB-APP-REGISTRATION.adocdeploy/RHODIBOT-DEPLOYMENT.adocdeploy/verify-rhodibot.shVerification, proven not asserted.
deploy/verify-rhodibot.shchecks theorigin's health and credential mode, that the process listens on loopback only,
that the tunnel reaches it, that an unsigned delivery is refused with 401, and
that the private endpoints 404. Run here against a live server, both directions:
The second run is a server deliberately started with
--bind 0.0.0.0; a checkthat cannot fail is not a check. The first draft of this script did fail
vacuously — it asked "who is listening on port 3000?" and flagged an unrelated
process that happened to share the port number. It now asks what it means:
where is rhodibot listening?
The unsigned-POST check is the one that matters most: with no webhook secret
configured the handler accepts anything, so a 401 proves both that the tunnel
reaches the origin and that the secret is set.
Start-up gate, observed:
GITHUB_APP_ID=123 rhodibot(no key) exits 1 withError: GITHUB_APP_ID is set but no private key was provided, rather thanserving unauthenticated traffic.
What remains human: the App registration in the browser (GitHub has no API for
it), the tunnel UUID, the zone hostname, and the
GITHUB_APP_IDin/etc/fleet/rhodibot.env.