fix(#2911): keep the forge token out of argv
`forge_git_url` spliced `core:<token>@` between scheme and authority, and that URL is a process argument. `/proc/<pid>/cmdline` is mode 0444 — world-readable — so the core admin token, which provisions every agent's forge account, was published to any local user for the lifetime of each git child. Seven call sites built such a URL. The credential now travels in the environment instead: `git_command_authed` sets `http.extraHeader` via `GIT_CONFIG_*`, which git reads exactly like a config file, and `/proc/<pid>/environ` is 0400 — owner-only. Same credential, materially smaller audience. The remote is a plain `http://forge/<org>/<repo>.git`, and `forge_git_url` no longer takes a token, so the old shape cannot be rebuilt by accident. `knowledge`'s clone was the one place a credentialed URL was stored as a named remote — git persists the clone URL into `.git/config`, so the token sat on disk and every later `pull` authenticated from there. That is the case `forge::repos::push_config` documents as forbidden ("the tokenised URL ... deliberately never stored as a named remote"). `pull` now rewrites `origin` to the plain URL first, which also scrubs the persisted token from existing deployments, and authenticates from the environment when a token is available. The repo is public, so the pull still works without one. Three call sites also stopped spawning `Command::new("git")` directly, so they honour the `HYPERHIVE_GIT` path the NixOS module bakes in and the `kill_on_drop` every other git spawn gets. The two URL-shape tests now assert the *absence* of a credential, and a new one decodes the header back to `core:<token>` — without that, a malformed header would leave every forge operation silently anonymous with the other assertions still green.
This commit is contained in:
parent
3617578341
commit
44572d1e1a
7 changed files with 134 additions and 56 deletions
|
|
@ -59,28 +59,34 @@ pub(crate) fn forge_http_base() -> &'static str {
|
|||
})
|
||||
}
|
||||
|
||||
/// Token-in-URL git remote for `repo` (e.g. `"core/meta"`). Inserts
|
||||
/// `core:<token>` credentials between the scheme and authority of
|
||||
/// [`forge_http_base()`] — the form git accepts for inline auth.
|
||||
pub(crate) fn forge_git_url(token: &str, repo: &str) -> String {
|
||||
git_url_with_base(forge_http_base(), token, repo)
|
||||
/// Git remote for `repo` (e.g. `"core/meta"`) — **credential-free**.
|
||||
///
|
||||
/// The token does not go here. A URL is a process argument, and `argv` is
|
||||
/// world-readable through `/proc/<pid>/cmdline` for as long as the git child
|
||||
/// lives, so a credentialed remote publishes the core admin token to every
|
||||
/// local user on the host. Credentials travel in the environment instead, via
|
||||
/// [`core_auth_header`] and [`crate::lifecycle::git_command_authed`] —
|
||||
/// `/proc/<pid>/environ` is owner-only.
|
||||
pub(crate) fn forge_git_url(repo: &str) -> String {
|
||||
git_url_with_base(forge_http_base(), repo)
|
||||
}
|
||||
|
||||
/// The credential-insertion half of [`forge_git_url`], split out so it
|
||||
/// can be tested without a process-wide env var (which would race every
|
||||
/// other test in this binary).
|
||||
/// The pure half of [`forge_git_url`], split out so it can be tested without a
|
||||
/// process-wide env var (which would race every other test in this binary).
|
||||
fn git_url_with_base(base: &str, repo: &str) -> String {
|
||||
format!("{base}/{repo}.git")
|
||||
}
|
||||
|
||||
/// The `http.extraHeader` value authenticating as the forge core user.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When `base` has no `://`. Previously this fell back to
|
||||
/// `http://core:<token>@localhost:3000` — a guess that would have sent
|
||||
/// a *credentialed* push at whatever answers on the local port. A
|
||||
/// malformed base is a broken deployment; failing on it is the point.
|
||||
fn git_url_with_base(base: &str, token: &str, repo: &str) -> String {
|
||||
let (scheme, host) = base
|
||||
.split_once("://")
|
||||
.unwrap_or_else(|| panic!("HIVE_FORGE_URL is not a URL (no \"://\"): {base}"));
|
||||
format!("{scheme}://core:{token}@{host}/{repo}.git")
|
||||
/// Basic auth over a header rather than userinfo in the URL, so the secret
|
||||
/// reaches git through the environment (see [`forge_git_url`]). Pair with
|
||||
/// [`crate::lifecycle::git_command_authed`], which is the only thing that
|
||||
/// should ever hold the result.
|
||||
pub(crate) fn core_auth_header(token: &str) -> String {
|
||||
use base64::Engine as _;
|
||||
let basic = base64::engine::general_purpose::STANDARD.encode(format!("core:{token}"));
|
||||
format!("Authorization: Basic {basic}")
|
||||
}
|
||||
|
||||
/// Forgejo org grouping every agent's config repo. Core is a site admin
|
||||
|
|
|
|||
Loading…
Reference in a new issue