Skip to content

feat(deploy): authenticate the checkout fetch so private repos can be previewed (#26) - #30

Merged
luthermonson merged 2 commits into
mainfrom
feat/authenticated-fetch-and-preview-gate
Sep 8, 2026
Merged

luthermonson merged 2 commits into
mainfrom
feat/authenticated-fetch-and-preview-gate

Conversation

@luthermonson

Copy link
Copy Markdown
Contributor

Closes #26 (Part A). Part B is designed and scoped as a companion ePHPm issue — see below.

Part A — authenticate the fetch (implemented)

deployer.rs::fetch_checkout ran git fetch against the plain https://github.com/<owner>/<repo>.git with no credential, so a private repository could not be checked out. It now uses the short-lived GitHub App installation token the daemon already mints (for PR comments, Deployment statuses, the claim-time PR-state check).

Mechanism — token kept out of persisted config and logs:

  • Injected as a transient http.extraheader: Authorization: Basic base64("x-access-token:<token>") via -c on the single git process it prefixes (fetch_auth_config + run_git_fetch). Never written to the checkout's persisted .git/config.
  • The credential lives in a separate auth prefix and is not part of the fetch_args slice — the only string a failed fetch joins into its error/log context. stderr stays discarded, so a git error can't leak it either.
  • The token is minted in handle_deploy (fetch_installation_token) with the same guards as the reporting path and passed into DeployContext.fetch_token.

Fail-open for public, clear failure for private: with no App credentials / no installation id the fetch stays unauthenticated — public repos work exactly as before; a private repo fails with a message naming --app-id/--app-key and the contents: read permission the installation needs, not a bare git error. (Confirm the App installation carries contents: read.)

Tests (src/deployer.rs):

  • a_token_becomes_a_scoped_extraheader_basic_credential — decodes to x-access-token:<token>.
  • no_token_means_no_credential — public path carries none.
  • the_token_is_kept_out_of_the_error_and_log_surface — token absent from the redacted error string (and base64-encoded, never literal, in the header).
  • private_repo_hint_only_when_unauthenticated.
  • an_authenticated_fetch_checks_out_and_never_persists_the_token — git-backed integration test: the code checks out and .git/config contains no token and no extraheader. Asserts the "not persisted" property directly.
  • a_public_repo_still_fetches_without_a_token — tokenless path still works.

Part B — gate the preview URL (designed, deferred to ePHPm)

A deployed preview is still world-readable. The clean enforcement point is ePHPm, not switchboard, so this PR ships the design rather than a half-gate:

  • The gate must cover the static-file path as well as PHP and fail closed. ePHPm already has exactly this — the request-phase middleware runs on both paths (static_request_phase, ephpm#395) before bytes leave disk. A prepend-based gate (switchboard's auto_prepend_file) runs only on the PHP path, so it's bypassable for static assets — worse than an honest gap.
  • switchboard's only per-site channel is the deliberately-closed two-key override file; it can't carry a per-preview credential, and [[middleware]] mounts are global.

Chosen mechanism: per-preview HTTP Basic auth, credential generated by switchboard and posted in the PR comment (visible only to users with repo read access), enforced by an ePHPm request-phase gate fed a per-site verifier via a new override key. Full threat model + alternatives (URL token, IP allowlist, GitHub OAuth) in docs/preview-access-gate.md.

Threat model: defends preview privacy (a random internet visitor who knows the hostname). Explicitly not hardened multi-tenant isolation, not protection against someone who already has repo access, not against a compromised reviewer account.

Scope split: this PR = Part A + design. Companion ePHPm issue = the request-phase per-site access gate. Follow-up switchboard PR (after the ePHPm key ships) = generate the credential, write the verifier, post it in the PR comment, rotate per deploy.

Docs

README documents the now-authenticated fetch and, honestly, the open preview-privacy gap pointing at the design doc.

Verification

cargo +nightly fmt --all --check, cargo clippy --all-targets -- -D warnings, and cargo test (223 pass, +6 new) all green.

Do not merge — for review.

… previewed (#26)

Part A — authenticate the fetch. `fetch_checkout` ran `git fetch` against the
plain https URL with no credential, so a private repository could not be checked
out. It now authenticates with the short-lived GitHub App installation token the
daemon already mints (used for PR comments and Deployment statuses):

- The token is injected as a transient `http.extraheader`
  (`Authorization: Basic base64("x-access-token:<token>")`) via `-c` on the
  single git process it prefixes — never written into the checkout's persisted
  `.git/config`.
- It is kept out of every log line and error message: the credential lives in a
  separate `auth` prefix, and the only string a failed fetch builds is
  `fetch_args.join(" ")`, which never contains it. stderr stays discarded.
- `None` (no App creds / no installation id) keeps today's behaviour: public
  repos still fetch unauthenticated; a private one fails with a clear message
  naming `--app-id`/`--app-key` and the `contents: read` permission, instead of
  a bare git error.

Tests: the credential decodes to `x-access-token:<token>`; the public path
carries none; the token is absent from the redacted error surface; and two
git-backed integration tests prove an authenticated fetch checks the code out
while leaving no token (and no `extraheader`) in the persisted git config, and
that a tokenless fetch still works.

Part B — gate the preview URL. A deployed preview is still world-readable. The
clean enforcement point is ePHPm's request-phase middleware (covers the
static-file path too and fails closed), not switchboard, whose only per-site
channel — the deliberately-closed two-key override file — cannot carry a
per-preview credential. So this delivers the written design + threat model in
docs/preview-access-gate.md and defers the switchboard-side credential work to a
follow-up gated on a companion ePHPm change, rather than shipping a
prepend-based gate that would be bypassable for static assets. README documents
the gap honestly.
@ephpm

ephpm Bot commented Sep 8, 2026

Copy link
Copy Markdown

ePHPm Preview — removed

Preview deployment has been torn down.

@ephpm

ephpm Bot commented Sep 8, 2026

Copy link
Copy Markdown

ePHPm Preview — deployed (health check pending)

URL https://ephpm-switchboard-pr-30.preview.ephpm.dev
Framework PHP
PHP 8.5
Deployed in 60.6s

Preview updates automatically on each push to this PR.

@ephpm

ephpm Bot commented Sep 8, 2026

Copy link
Copy Markdown

ePHPm Preview — deployed (health check pending)

URL https://ephpm-switchboard-pr-30.preview.ephpm.dev
Framework PHP
PHP 8.5
Deployed in 60.7s

Preview updates automatically on each push to this PR.

@ephpm
ephpm Bot temporarily deployed to preview-pr-30 September 8, 2026 14:39 Inactive
@ephpm
ephpm Bot temporarily deployed to preview-pr-30 September 8, 2026 14:39 Inactive
@ephpm
ephpm Bot temporarily deployed to preview-pr-30 September 8, 2026 14:39 Inactive
@ephpm
ephpm Bot temporarily deployed to preview-pr-30 September 8, 2026 14:42 Inactive
@ephpm
ephpm Bot temporarily deployed to preview-pr-30 September 8, 2026 14:42 Inactive
@luthermonson
luthermonson merged commit f278118 into main Sep 8, 2026
4 checks passed
@luthermonson
luthermonson deleted the feat/authenticated-fetch-and-preview-gate branch September 8, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

private repositories cannot be previewed: the checkout fetch is unauthenticated

1 participant