Add stdin bearer authentication for exec-server registration (#59254)

## Why

Allow remote exec-server registration to use credentials independently of Codex login while retaining the usual local configuration and plugins.

## What changed

- Add `--registration-bearer-stdin` to read one newline-terminated bearer and use it for remote registration.
- Validate the bearer, require HTTPS or HTTP loopback for the registry, and disable process dumpability on Linux before reading the credential.
- Document the option and its distinction from rotating credentials via `--executor-credential-stdin`.

## Testing

Add integration coverage for registration with the supplied bearer across desktop sign-in states, rejection of non-loopback HTTP registries, and restricted process environment access on Linux. Update CLI help snapshots.

GitOrigin-RevId: da99492270c5dc6bb5e0cac41947e9c999a72b75
This commit is contained in:
Anton Panasenko
2026-11-03 23:22:28 +00:00
committed by copyberry
parent f2e0bf2d62
commit 1176913a6f
8 changed files with 167 additions and 2 deletions
+17 -1
View File
@@ -106,6 +106,15 @@ pub struct ExecServerCommand {
)]
use_agent_identity_auth: bool,
/// Read one registration bearer from stdin.
#[arg(
long,
requires = "exec_server_remote",
conflicts_with_all = ["use_agent_identity_auth", "executor_credential_stdin", "aws_sigv4"],
global = true
)]
registration_bearer_stdin: bool,
/// Read rotating executor credentials from stdin; exit when the pipe closes.
#[arg(
long = "executor-credential-stdin",
@@ -194,6 +203,10 @@ impl ExecServerCommand {
let environment_id = self.environment_id.take().ok_or_else(|| {
anyhow::anyhow!("--environment-id is required when --remote is set")
})?;
let registration_auth = self
.registration_bearer_stdin
.then(codex_exec_server::registration_auth_from_stdin)
.transpose()?;
let config =
load_exec_server_config(root_config_overrides, strict_config, enable_cloud_config)
.await?;
@@ -228,6 +241,8 @@ impl ExecServerCommand {
config.http_client_factory(),
)
.await?
} else if let Some(provider) = registration_auth {
provider
} else {
load_exec_server_remote_auth_provider(
&config,
@@ -247,7 +262,7 @@ impl ExecServerCommand {
auth_provider,
config.http_client_factory(),
)?;
if self.executor_credential_stdin {
if self.executor_credential_stdin || self.registration_bearer_stdin {
remote_config = remote_config.with_executor_credential_auth()?;
}
if let Some(name) = self.name {
@@ -342,6 +357,7 @@ impl ExecServerCommand {
self.remote.is_some()
&& self.command.is_none()
&& !self.executor_credential_stdin
&& !self.registration_bearer_stdin
&& !self.aws_sigv4
&& !self.use_agent_identity_auth
}
@@ -93,6 +93,9 @@ Options:
Use Agent Identity auth from CODEX_ACCESS_TOKEN for remote
registration
--registration-bearer-stdin
Read one registration bearer from stdin
--executor-credential-stdin
Read rotating executor credentials from stdin; exit when the pipe
closes
@@ -21,6 +21,110 @@ use std::process::Stdio;
use tokio::io::AsyncBufReadExt;
use tokio::io::AsyncWriteExt;
use tokio::io::BufReader;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::header;
use wiremock::matchers::method;
use wiremock::matchers::path;
#[tokio::test]
async fn registration_bearer_rejects_plaintext_remote_registry() -> anyhow::Result<()> {
let binary = cargo_bin("codex-exec-server")?;
let home = tempfile::tempdir()?;
let mut child = tokio::process::Command::from(background_command(&binary))
.args([
"--remote",
"http://registry.example.invalid",
"--environment-id",
"test",
"--registration-bearer-stdin",
])
.env("CODEX_HOME", home.path())
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()?;
child
.stdin
.take()
.context("registration stdin")?
.write_all(b"registration-placeholder\n")
.await?;
let output = child.wait_with_output().await?;
assert!(!output.status.success());
assert!(
String::from_utf8(output.stderr)?.contains("requires https for non-loopback endpoints")
);
Ok(())
}
#[tokio::test]
async fn registration_uses_its_bearer_before_and_after_desktop_sign_in() -> anyhow::Result<()> {
let binary = cargo_bin("codex-exec-server")?;
let home = tempfile::tempdir()?;
for (desktop_token, account_id) in [
("access_token_initial", None),
("signed-in-token", Some("desktop-account")),
] {
std::fs::write(
home.path().join("auth.json"),
serde_json::to_vec(&json!({
"auth_mode": "chatgpt",
"tokens": {
"id_token": "eyJhbGciOiJub25lIn0.e30.signature",
"access_token": desktop_token,
"refresh_token": "refresh_token",
"account_id": account_id
}
}))?,
)?;
let registry = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/cloud/environment/test/register"))
.and(header("Authorization", "Bearer registration-placeholder"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"environment_id": "test",
"url": format!("{}/relay", registry.uri().replacen("http://", "ws://", 1)),
"security_profile": "noise_hybrid_ik_v1",
"executor_registration_id": "registration-1"
})))
.mount(&registry)
.await;
// Connecting to the returned relay proves the executor accepted registration.
let relay = Mock::given(method("GET"))
.and(path("/relay"))
.respond_with(ResponseTemplate::new(403))
.expect(1)
.mount_as_scoped(&registry)
.await;
let mut server = tokio::process::Command::from(background_command(&binary))
.args(["--remote", &registry.uri(), "--environment-id", "test"])
.arg("--registration-bearer-stdin")
.stdin(Stdio::piped())
.env("CODEX_HOME", home.path())
.kill_on_drop(true)
.spawn()?;
server
.stdin
.take()
.context("registration stdin")?
.write_all(b"registration-placeholder\n")
.await?;
tokio::time::timeout(
std::time::Duration::from_secs(15),
relay.wait_until_satisfied(),
)
.await?;
#[cfg(target_os = "linux")]
{
let pid = server.id().context("executor PID")?;
let error = std::fs::File::open(format!("/proc/{pid}/environ")).unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
}
}
Ok(())
}
#[tokio::test]
async fn standalone_initializes_reports_version_and_runs_helpers() -> anyhow::Result<()> {