Remove secrets from the bootstrap wire format
Bootstrap no longer carries secrets — quire-ci always resolves them
through the API. Removes the secrets field from Bootstrap, the
write_bootstrap/secrets-revealing logic, the RunContext secrets
pass-through, and the local quire-ci config-loading of secrets.
Assisted-by: Owl Alpha via pi
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index b94f3e2..d4c5e7a 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -58,7 +58,7 @@ enum Commands {
///
/// `--bootstrap <path>` points at a JSON file (see
/// [`quire_core::ci::bootstrap::Bootstrap`]) produced by the
- /// orchestrator that supplies push metadata and secrets.
+ /// orchestrator that supplies push metadata.
Run {
/// Where to send the structured event stream. Accepts:
/// `null` — drop events (default).
@@ -76,7 +76,7 @@ enum Commands {
out_dir: Option<PathBuf>,
/// Path to a JSON bootstrap file produced by the orchestrator.
- /// Carries push metadata and the secrets the run-fns may resolve.
+ /// Carries push metadata.
#[arg(long)]
bootstrap: PathBuf,
@@ -564,7 +564,6 @@ fn compile_at(workspace: &std::path::Path) -> Result<Pipeline> {
#[cfg(test)]
mod tests {
use super::*;
- use std::collections::HashMap;
#[test]
fn parse_events_target_classifies_input() {
@@ -593,7 +592,6 @@ mod tests {
pushed_at: jiff::Timestamp::now(),
},
git_dir: PathBuf::from("/tmp/repo.git"),
- secrets: HashMap::new(),
sentry: None,
};
fs_err::write(&path, serde_json::to_vec(&bootstrap).unwrap()).expect("write");
diff --git a/quire-core/src/ci/bootstrap.rs b/quire-core/src/ci/bootstrap.rs
index ad90dbd..273783f 100644
--- a/quire-core/src/ci/bootstrap.rs
+++ b/quire-core/src/ci/bootstrap.rs
@@ -3,19 +3,14 @@
//!
//! The orchestrator writes a [`Bootstrap`] as JSON to a file inside
//! the run directory and passes the path via `--bootstrap`. `quire-ci`
-//! deserializes it on startup to recover push metadata and the
-//! secrets the run-fns may resolve. Standalone `quire-ci run`
-//! invocations skip the file entirely and fall back to placeholder
-//! values.
+//! deserializes it on startup to recover push metadata. Standalone
+//! `quire-ci run` invocations skip the file entirely and fall back to
+//! placeholder values.
//!
-//! The file contains live secret values; callers must restrict
-//! permissions (mode 0600 on Unix) before writing. The file is a
-//! one-shot handoff: `quire-ci` unlinks it as soon as it has read
-//! the bytes into memory, and the orchestrator best-effort unlinks
-//! after the subprocess exits as a safety net. Plaintext secrets
-//! should never persist in the run directory.
+//! The file is a one-shot handoff: `quire-ci` unlinks it as soon as
+//! it has read the bytes into memory, and the orchestrator
+//! best-effort unlinks after the subprocess exits as a safety net.
-use std::collections::HashMap;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
@@ -24,12 +19,6 @@ use crate::ci::run::RunMeta;
/// Inputs the orchestrator supplies to a quire-ci subprocess.
///
-/// Secret values cross as plaintext — `SecretString` deliberately
-/// doesn't implement `Serialize` to avoid accidental leaks. The
-/// orchestrator reveals values into this map before writing the
-/// file (mode 0600); quire-ci wraps them back into `SecretString`s
-/// on read.
-///
/// `git_dir` is the bare repo the run is scoped to. quire-ci surfaces
/// it via `(jobs :quire/push).git-dir`, which the mirror job's run-fn
/// passes to git as `GIT_DIR`. The materialized workspace is a flat
@@ -39,7 +28,6 @@ use crate::ci::run::RunMeta;
pub struct Bootstrap {
pub meta: RunMeta,
pub git_dir: PathBuf,
- pub secrets: HashMap<String, String>,
/// Sentry handoff, present only when the orchestrator's global
/// config sets a DSN. Carries the matching trace id so both
/// sides' events land on the same trace.
@@ -49,8 +37,7 @@ pub struct Bootstrap {
/// What quire-ci needs to mirror the orchestrator's Sentry context.
///
-/// The DSN is plaintext like the secrets — the 0600 mode on the
-/// bootstrap file is the line of defense. `trace_id` is the hex form
+/// `trace_id` is the hex form
/// of [`sentry::protocol::TraceId`]; kept as a string here so
/// `quire-core` doesn't grow a `sentry` dep.
#[derive(Debug, Serialize, Deserialize)]
diff --git a/quire-server/src/bin/quire/commands/ci.rs b/quire-server/src/bin/quire/commands/ci.rs
index 93947a1..56d4971 100644
--- a/quire-server/src/bin/quire/commands/ci.rs
+++ b/quire-server/src/bin/quire/commands/ci.rs
@@ -45,11 +45,10 @@ pub async fn run(quire: &Quire, maybe_sha: Option<&str>) -> Result<()> {
let commit = resolve_commit(maybe_sha)?;
let ci = Ci::new(repo_path.clone());
- // Pull secrets from the global config; absence is fine for local
- // testing. A broken-but-present config is a real error.
- let secrets = match quire.global_config() {
- Ok(c) => c.secrets,
- Err(quire::Error::ConfigNotFound(_)) => std::collections::HashMap::new(),
+ // Ensure the global config is valid; absence is fine for local testing.
+ match quire.global_config() {
+ Ok(_) => {}
+ Err(quire::Error::ConfigNotFound(_)) => {}
Err(e) => return Err(e).into_diagnostic(),
};
@@ -86,14 +85,7 @@ pub async fn run(quire: &Quire, maybe_sha: Option<&str>) -> Result<()> {
let workspace = tmp.path().join("workspace");
quire::ci::materialize_workspace(&repo_path.join(".git"), &commit.sha, &workspace)
.into_diagnostic()?;
- let exec_result = run.execute(
- &repo_path.join(".git"),
- &workspace,
- &meta,
- &secrets,
- None,
- None,
- );
+ let exec_result = run.execute(&repo_path.join(".git"), &workspace, &meta, None, None);
// Print the combined quire-ci log regardless of outcome.
let log_path = tmp.path().join(&run_id).join("quire-ci.log");
diff --git a/quire-server/src/ci/mod.rs b/quire-server/src/ci/mod.rs
index 203c34e..278a60e 100644
--- a/quire-server/src/ci/mod.rs
+++ b/quire-server/src/ci/mod.rs
@@ -1,7 +1,5 @@
//! CI: trigger runs from push events, validate the job graph.
-use std::collections::HashMap;
-
mod run;
pub(crate) mod error;
@@ -114,7 +112,6 @@ struct TriggerContext<'a> {
struct RunContext<'a> {
repo: &'a Repo,
db_path: &'a Path,
- secrets: &'a HashMap<String, quire_core::secret::SecretString>,
executor: Executor,
}
@@ -157,7 +154,6 @@ pub fn trigger(quire: &crate::Quire, event: &PushEvent) {
run: RunContext {
repo: &repo,
db_path: &db_path,
- secrets: &config.secrets,
executor: config.ci.executor,
},
event_repo: &event.repo,
@@ -262,14 +258,7 @@ fn run_ref_inner(
Executor::Process => {
// Compilation happens inside quire-ci so a malformed ci.fnl is
// reported once, with the worker's trace context.
- run.execute(
- &ctx.repo.path(),
- &workspace,
- &meta,
- ctx.secrets,
- sentry,
- Some(transport),
- )?;
+ run.execute(&ctx.repo.path(), &workspace, &meta, sentry, Some(transport))?;
}
}
Ok(())
@@ -417,15 +406,10 @@ mod tests {
assert!(result.is_err(), "bad Fennel should fail");
}
- fn run_ctx<'a>(
- repo: &'a crate::quire::Repo,
- db_path: &'a std::path::Path,
- secrets: &'a HashMap<String, quire_core::secret::SecretString>,
- ) -> RunContext<'a> {
+ fn run_ctx<'a>(repo: &'a crate::quire::Repo, db_path: &'a std::path::Path) -> RunContext<'a> {
RunContext {
repo,
db_path,
- secrets,
executor: Executor::Process,
}
}
@@ -517,8 +501,7 @@ exit 0
let (_fake_dir, fake_path) = fake_quire_ci(0);
let db_path = quire.db_path();
- let secrets = HashMap::new();
- let ctx = run_ctx(&repo, &db_path, &secrets);
+ let ctx = run_ctx(&repo, &db_path);
let trigger_result = with_path(&fake_path, || {
run_ref_inner(
&ctx,
@@ -572,8 +555,7 @@ exit 0
let (_fake_dir, fake_path) = fake_quire_ci(1);
let db_path = quire.db_path();
- let secrets = HashMap::new();
- let ctx = run_ctx(&repo, &db_path, &secrets);
+ let ctx = run_ctx(&repo, &db_path);
let trigger_result = with_path(&fake_path, || {
run_ref_inner(
&ctx,
@@ -617,8 +599,7 @@ exit 0
};
let db_path = quire.db_path();
- let secrets = HashMap::new();
- let ctx = run_ctx(&repo, &db_path, &secrets);
+ let ctx = run_ctx(&repo, &db_path);
run_ref_inner(
&ctx,
pushed_at,
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index 121d53c..0d757d8 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -10,7 +10,6 @@ use std::path::{Path, PathBuf};
use jiff::Timestamp;
use quire_core::ci::transport::ApiSession;
-use quire_core::secret::SecretString;
use rand::{Rng, distr::Alphanumeric};
use super::error::{Error, Result};
@@ -307,7 +306,6 @@ impl Run {
git_dir: &Path,
workspace: &Path,
meta: &RunMeta,
- secrets: &HashMap<String, SecretString>,
sentry: Option<&quire_core::ci::bootstrap::SentryHandoff>,
transport: Option<&Transport>,
) -> Result<()> {
@@ -322,7 +320,7 @@ impl Run {
let log = fs_err::File::create(&log_path)?.into_parts().0;
let log_clone = log.try_clone()?;
- write_bootstrap(&bootstrap_path, git_dir, meta, secrets, sentry)?;
+ write_bootstrap(&bootstrap_path, git_dir, meta, sentry)?;
tracing::info!(
run_id = %self.id,
@@ -628,29 +626,18 @@ impl Run {
}
/// Serialize the bootstrap payload as JSON and write it to `path` with
-/// owner-only permissions on Unix. Secrets cross as plaintext so the
-/// 0600 mode is the line of defense against other local users; failure
-/// to set the mode aborts the write (better than leaking).
+/// owner-only permissions on Unix.
fn write_bootstrap(
path: &Path,
git_dir: &Path,
meta: &RunMeta,
- secrets: &HashMap<String, SecretString>,
sentry: Option<&quire_core::ci::bootstrap::SentryHandoff>,
) -> Result<()> {
use quire_core::ci::bootstrap::{Bootstrap, SentryHandoff};
- let mut revealed: HashMap<String, String> = HashMap::with_capacity(secrets.len());
- for (name, value) in secrets {
- revealed.insert(
- name.clone(),
- value.reveal().map_err(Error::Secret)?.to_string(),
- );
- }
let bootstrap = Bootstrap {
meta: meta.clone(),
git_dir: git_dir.to_path_buf(),
- secrets: revealed,
sentry: sentry.map(|s| SentryHandoff {
dsn: s.dsn.clone(),
trace_id: s.trace_id.clone(),
@@ -867,14 +854,7 @@ mod tests {
let bootstrap_path = dir.path().join("bootstrap.json");
let git_dir = dir.path().join("repos").join("test.git");
- write_bootstrap(
- &bootstrap_path,
- &git_dir,
- &test_meta(),
- &HashMap::new(),
- None,
- )
- .expect("write_bootstrap");
+ write_bootstrap(&bootstrap_path, &git_dir, &test_meta(), None).expect("write_bootstrap");
let bytes = fs_err::read(&bootstrap_path).expect("read bootstrap");
let bootstrap: Bootstrap = serde_json::from_slice(&bytes).expect("parse bootstrap");