Support base64-encoded PEM for ARC_JWT_PUBLIC_KEY and ARC_JWT_PRIVATE_KEY

Some deployment environments (e.g. container orchestrators) make it
easier to pass secrets as single-line base64 strings rather than
multi-line PEM. Both env vars now auto-detect the format: if the value
starts with "-----" it's treated as raw PEM, otherwise base64-decoded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-03 14:39:49 -05:00
parent b7f69a6b13
commit d5b98af6d1
4 changed files with 23 additions and 4 deletions

1
Cargo.lock generated
View file

@ -141,6 +141,7 @@ dependencies = [
"arc-util",
"arc-workflows",
"axum",
"base64",
"clap",
"dirs",
"http-body-util",

View file

@ -3,6 +3,11 @@ import { getAppConfig } from "./lib/config.server";
const ARC_JWT_PRIVATE_KEY = process.env.ARC_JWT_PRIVATE_KEY;
function decodePemEnv(value: string): string {
if (value.startsWith("-----")) return value;
return Buffer.from(value, "base64").toString("utf-8");
}
let cachedKey: CryptoKey | null = null;
async function getSigningKey(): Promise<CryptoKey> {
@ -10,7 +15,7 @@ async function getSigningKey(): Promise<CryptoKey> {
if (!ARC_JWT_PRIVATE_KEY) {
throw new Error("ARC_JWT_PRIVATE_KEY environment variable is not set");
}
cachedKey = await importPKCS8(ARC_JWT_PRIVATE_KEY, "EdDSA");
cachedKey = await importPKCS8(decodePemEnv(ARC_JWT_PRIVATE_KEY), "EdDSA");
return cachedKey;
}

View file

@ -20,6 +20,7 @@ dirs.workspace = true
sqlx.workspace = true
tower = "0.5"
tokio-stream = { workspace = true, features = ["sync"] }
base64.workspace = true
jsonwebtoken.workspace = true
tokio.workspace = true
serde.workspace = true

View file

@ -26,6 +26,16 @@ pub enum AuthMode {
Disabled,
}
/// Decode a PEM env var that may be raw PEM or base64-encoded PEM.
fn decode_pem_env(name: &str, value: &str) -> String {
if value.starts_with("-----") {
return value.to_string();
}
let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, value)
.unwrap_or_else(|e| panic!("{name} is not valid PEM or base64: {e}"));
String::from_utf8(bytes).unwrap_or_else(|e| panic!("{name} base64 decoded to invalid UTF-8: {e}"))
}
/// Resolve the authentication mode from the API config section.
///
/// Call this once at startup before serving requests. Panics if the
@ -39,13 +49,15 @@ pub fn resolve_auth_mode(api_config: &crate::server_config::ApiConfig) -> AuthMo
AuthMode::Disabled
}
ApiAuthenticationStrategy::Jwt => {
let pem = std::env::var("ARC_JWT_PUBLIC_KEY").unwrap_or_else(|_| {
let raw = std::env::var("ARC_JWT_PUBLIC_KEY").unwrap_or_else(|_| {
panic!(
"ARC_JWT_PUBLIC_KEY is not set. Either provide an Ed25519 public key in PEM \
format or set authentication_strategy = \"insecure_disabled\" in \
~/.arc/arc.toml to allow unauthenticated access (development only)."
format (or base64-encoded PEM) or set authentication_strategy = \
\"insecure_disabled\" in ~/.arc/arc.toml to allow unauthenticated access \
(development only)."
)
});
let pem = decode_pem_env("ARC_JWT_PUBLIC_KEY", &raw);
let key = DecodingKey::from_ed_pem(pem.as_bytes())
.expect("ARC_JWT_PUBLIC_KEY contains an invalid Ed25519 PEM public key");
AuthMode::Jwt(Arc::new(key))