mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
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:
committed by
copyberry
parent
f2e0bf2d62
commit
1176913a6f
@@ -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
|
||||
}
|
||||
|
||||
+3
@@ -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(®istry)
|
||||
.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(®istry)
|
||||
.await;
|
||||
let mut server = tokio::process::Command::from(background_command(&binary))
|
||||
.args(["--remote", ®istry.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<()> {
|
||||
|
||||
Reference in New Issue
Block a user