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
|
|
@ -17,7 +17,7 @@ use anyhow::{Context, Result};
|
|||
use forgejo_api::structs::{CreateHookOption, CreateHookOptionConfig, CreateHookOptionType};
|
||||
|
||||
use crate::coordinator::Coordinator;
|
||||
use crate::forge::forge_git_url;
|
||||
use crate::forge::{core_auth_header, forge_git_url};
|
||||
|
||||
pub const ORG: &str = "internal";
|
||||
pub const REPO: &str = "knowledge";
|
||||
|
|
@ -73,8 +73,8 @@ pub async fn ensure_local_clone(core_token: &str) -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
std::fs::create_dir_all(LOCAL_DIR).context("create knowledge local dir")?;
|
||||
let url = forge_git_url(core_token, &format!("{ORG}/{REPO}"));
|
||||
let out = tokio::process::Command::new("git")
|
||||
let url = forge_git_url(&format!("{ORG}/{REPO}"));
|
||||
let out = crate::lifecycle::git_command_authed(&core_auth_header(core_token))
|
||||
.args(["clone", &url, LOCAL_DIR])
|
||||
.output()
|
||||
.await
|
||||
|
|
@ -127,8 +127,8 @@ async fn seed_readme(core_token: &str) -> Result<()> {
|
|||
anyhow::bail!("git {args:?} failed: {stderr}");
|
||||
}
|
||||
}
|
||||
let url = forge_git_url(core_token, &format!("{ORG}/{REPO}"));
|
||||
let out = tokio::process::Command::new("git")
|
||||
let url = forge_git_url(&format!("{ORG}/{REPO}"));
|
||||
let out = crate::lifecycle::git_command_authed(&core_auth_header(core_token))
|
||||
.args(["-C", LOCAL_DIR, "push", &url, "HEAD:main"])
|
||||
.output()
|
||||
.await
|
||||
|
|
@ -273,8 +273,26 @@ pub async fn pull(coord: &Coordinator) -> Result<()> {
|
|||
if !git_dir.exists() {
|
||||
anyhow::bail!("knowledge: {LOCAL_DIR}/.git not found — clone first");
|
||||
}
|
||||
// Rewrite any credentialed `origin` left by an older clone, which spliced
|
||||
// `core:<token>@` into the URL and persisted it in `.git/config` — the one
|
||||
// place this repo stored a token in a named remote, against the rule
|
||||
// `forge::repos::push_config` states. Harmless to repeat once the URL is
|
||||
// already clean.
|
||||
let plain = crate::forge::forge_git_url(&format!("{ORG}/{REPO}"));
|
||||
let _ = crate::lifecycle::git_command()
|
||||
.args(["-C", LOCAL_DIR, "remote", "set-url", "origin", &plain])
|
||||
.output()
|
||||
.await;
|
||||
let before = head_sha().await;
|
||||
let out = tokio::process::Command::new("git")
|
||||
// The repo is public (`ensure_knowledge_repo` makes it so), so an
|
||||
// unauthenticated pull is enough — but authenticate when a token is around,
|
||||
// which keeps this working if the repo is ever made private again.
|
||||
let auth = crate::forge::core_token().map(|t| core_auth_header(&t));
|
||||
let mut cmd = match &auth {
|
||||
Some(header) => crate::lifecycle::git_command_authed(header),
|
||||
None => crate::lifecycle::git_command(),
|
||||
};
|
||||
let out = cmd
|
||||
.args(["-C", LOCAL_DIR, "pull", "--ff-only"])
|
||||
.output()
|
||||
.await
|
||||
|
|
|
|||
Loading…
Reference in a new issue