diff --git a/.github/workflows/auth-proxy.yaml b/.github/workflows/auth-proxy.yaml deleted file mode 100644 index 9959bc2..0000000 --- a/.github/workflows/auth-proxy.yaml +++ /dev/null @@ -1,61 +0,0 @@ -name: auth-proxy image - -on: - push: - branches: - - main - tags: - - "*" - pull_request: - paths: - - "auth-proxy/**" - - "docker/Dockerfile.auth-proxy" - - ".github/workflows/auth-proxy.yaml" - - "Cargo.toml" - - "Cargo.lock" - workflow_dispatch: - -permissions: - contents: read - -jobs: - docker: - name: Build and push auth-proxy image - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - name: Login to GHCR - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository_owner }}/cli-auth-proxy - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile.auth-proxy - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: | - type=registry,ref=ghcr.io/${{ github.repository_owner }}/cli-auth-proxy:main - type=gha - cache-to: type=gha,mode=max diff --git a/Cargo.lock b/Cargo.lock index e5f8206..92478c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,12 +137,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - [[package]] name = "aqora" version = "0.24.1" @@ -235,29 +229,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "aqora-auth-proxy" -version = "0.24.1" -dependencies = [ - "anyhow", - "axum", - "base64 0.22.1", - "bytes", - "clap", - "futures", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "pem", - "reqwest", - "ring", - "thiserror 1.0.69", - "tokio", - "tracing", - "tracing-subscriber", - "url", -] - [[package]] name = "aqora-client" version = "0.24.1" @@ -3728,16 +3699,6 @@ dependencies = [ "hmac", ] -[[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64 0.22.1", - "serde_core", -] - [[package]] name = "pep440_rs" version = "0.7.3" @@ -4801,11 +4762,10 @@ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ - "serde_core", "serde_derive", ] @@ -4835,20 +4795,11 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index ca6d302..1a5ebd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["archiver", "config", "runner", "template", "data-utils", "client", "auth-proxy"] +members = ["archiver", "config", "runner", "template", "data-utils", "client"] [workspace.package] version = "0.24.1" diff --git a/auth-proxy/Cargo.toml b/auth-proxy/Cargo.toml deleted file mode 100644 index 136f93e..0000000 --- a/auth-proxy/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "aqora-auth-proxy" -version.workspace = true -license.workspace = true -edition = "2021" -publish = false - -[lib] -name = "aqora_auth_proxy" -path = "src/lib.rs" - -[[bin]] -name = "auth-proxy" -path = "src/main.rs" - -[dependencies] -http = "1.1" -http-body = "1.0.1" -http-body-util = "0.1.3" -bytes = "1.10" -base64 = "0.22" -ring = "0.17.14" -thiserror = "1.0" -pem = "3" - -# bin-only -tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "net"] } -axum = "0.7" -reqwest = { version = "0.12", default-features = false, features = ["stream", "rustls-tls"] } -clap = { version = "4.4", features = ["derive"] } -url = "2.5" -anyhow = "1" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -futures = "0.3" - -[dev-dependencies] -tokio = { version = "1", features = ["macros", "rt"] } diff --git a/auth-proxy/src/lib.rs b/auth-proxy/src/lib.rs deleted file mode 100644 index 2c63ead..0000000 --- a/auth-proxy/src/lib.rs +++ /dev/null @@ -1,333 +0,0 @@ -use base64::prelude::*; -use bytes::Bytes; -use http::{HeaderMap, HeaderName, HeaderValue, Method, Request}; -use http_body_util::BodyExt; -use ring::signature::KeyPair; - -pub type BoxError = Box; - -pub const SIG_HEADER_NAME_STR: &str = "x-auth-proxy-sig"; - -pub fn sig_header_name() -> HeaderName { - HeaderName::from_static(SIG_HEADER_NAME_STR) -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("X-Auth-Proxy-Sig header already present")] - SignatureAlreadyPresent, - #[error("missing X-Auth-Proxy-Sig header")] - SignatureMissing, - #[error("invalid signature header: {0}")] - SignatureMalformed(&'static str), - #[error("signature verification failed")] - SignatureInvalid, - #[error("body collect error: {0}")] - Body(#[source] BoxError), - #[error("invalid PEM: {0}")] - Pem(String), - #[error("invalid PKCS#8 Ed25519 key")] - InvalidKey, -} - -pub struct SigningKey(ring::signature::Ed25519KeyPair); - -impl SigningKey { - pub fn from_pkcs8_pem(pem_str: &str) -> Result { - let parsed = pem::parse(pem_str).map_err(|e| Error::Pem(e.to_string()))?; - if parsed.tag() != "PRIVATE KEY" { - return Err(Error::Pem(format!( - "expected `PRIVATE KEY` (PKCS#8) tag, got `{}`", - parsed.tag() - ))); - } - Self::from_pkcs8_der(parsed.contents()) - } - - pub fn from_pkcs8_der(der: &[u8]) -> Result { - ring::signature::Ed25519KeyPair::from_pkcs8_maybe_unchecked(der) - .map(SigningKey) - .map_err(|_| Error::InvalidKey) - } - - pub fn verifying_key(&self) -> VerifyingKey { - let mut bytes = [0u8; 32]; - bytes.copy_from_slice(self.0.public_key().as_ref()); - VerifyingKey(bytes) - } -} - -#[derive(Clone)] -pub struct VerifyingKey([u8; 32]); - -impl VerifyingKey { - pub fn from_bytes(bytes: [u8; 32]) -> Self { - Self(bytes) - } - - pub fn as_bytes(&self) -> &[u8; 32] { - &self.0 - } -} - -const HOP_BY_HOP: &[&str] = &[ - "connection", - "keep-alive", - "proxy-authenticate", - "proxy-authorization", - "te", - "trailers", - "transfer-encoding", - "upgrade", -]; - -fn is_excluded(name: &HeaderName) -> bool { - let s = name.as_str(); - s == SIG_HEADER_NAME_STR || s == "host" || s == "content-length" || HOP_BY_HOP.contains(&s) -} - -/// Build a deterministic byte representation of the request that can be signed -/// or verified. Header names are already lowercase per the `http` crate's -/// `HeaderName` invariant; we sort by `(name, value)` so multi-value headers -/// inserted in any order produce identical output. -/// -/// Excluded from the signed set: `x-auth-proxy-sig`, `host`, `content-length`, -/// and standard hop-by-hop headers. -// -// NOTE: We roll our own simple format rather than RFC 9421 (HTTP Message -// Signatures); revisit if interop with other implementations becomes a -// requirement. -pub fn canonicalize(method: &Method, target: &str, headers: &HeaderMap, body: &[u8]) -> Vec { - let mut out = Vec::with_capacity(64 + body.len()); - out.extend_from_slice(method.as_str().as_bytes()); - out.push(b' '); - out.extend_from_slice(target.as_bytes()); - out.push(b'\n'); - - let mut pairs: Vec<(&[u8], &[u8])> = headers - .iter() - .filter(|(name, _)| !is_excluded(name)) - .map(|(name, value)| (name.as_str().as_bytes(), value.as_bytes())) - .collect(); - pairs.sort(); - for (name, value) in pairs { - out.extend_from_slice(name); - out.extend_from_slice(b": "); - out.extend_from_slice(value); - out.push(b'\n'); - } - - out.push(b'\n'); - out.extend_from_slice(body); - out -} - -/// Sign a request, attaching the `X-Auth-Proxy-Sig` header. The body is -/// buffered into memory so it can be both signed and forwarded; the returned -/// request carries the body as `Bytes`. -/// -/// Errors with `Error::SignatureAlreadyPresent` if the header is already set. -pub async fn sign(req: Request, key: &SigningKey) -> Result, Error> -where - B: http_body::Body, - B::Error: Into, -{ - let sig_name = sig_header_name(); - if req.headers().contains_key(&sig_name) { - return Err(Error::SignatureAlreadyPresent); - } - let (mut parts, body) = req.into_parts(); - let body_bytes = body - .collect() - .await - .map_err(|e| Error::Body(e.into()))? - .to_bytes(); - - let target = parts - .uri - .path_and_query() - .map(|pq| pq.as_str()) - .unwrap_or("/"); - let canonical = canonicalize(&parts.method, target, &parts.headers, &body_bytes); - - let signature = key.0.sign(&canonical); - let encoded = BASE64_URL_SAFE_NO_PAD.encode(signature.as_ref()); - let value = - HeaderValue::try_from(encoded).expect("base64-url-safe is always a valid header value"); - parts.headers.insert(sig_name, value); - - Ok(Request::from_parts(parts, body_bytes)) -} - -/// Verify a signed request. Returns the request with the body buffered as -/// `Bytes` and the signature header preserved. -pub async fn verify(req: Request, key: &VerifyingKey) -> Result, Error> -where - B: http_body::Body, - B::Error: Into, -{ - let sig_name = sig_header_name(); - let sig_value = req - .headers() - .get(&sig_name) - .ok_or(Error::SignatureMissing)? - .clone(); - let sig_str = sig_value - .to_str() - .map_err(|_| Error::SignatureMalformed("not ASCII"))?; - let sig_bytes = BASE64_URL_SAFE_NO_PAD - .decode(sig_str) - .map_err(|_| Error::SignatureMalformed("not base64url-no-pad"))?; - - let (mut parts, body) = req.into_parts(); - let body_bytes = body - .collect() - .await - .map_err(|e| Error::Body(e.into()))? - .to_bytes(); - - parts.headers.remove(&sig_name); - - let target = parts - .uri - .path_and_query() - .map(|pq| pq.as_str()) - .unwrap_or("/"); - let canonical = canonicalize(&parts.method, target, &parts.headers, &body_bytes); - - let public_key = - ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, key.0.as_ref()); - public_key - .verify(&canonical, &sig_bytes) - .map_err(|_| Error::SignatureInvalid)?; - - parts.headers.insert(sig_name, sig_value); - - Ok(Request::from_parts(parts, body_bytes)) -} - -#[cfg(test)] -mod tests { - use super::*; - use http_body_util::Full; - - fn fresh_key() -> SigningKey { - let rng = ring::rand::SystemRandom::new(); - let pkcs8 = ring::signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap(); - SigningKey::from_pkcs8_der(pkcs8.as_ref()).unwrap() - } - - fn req(method: Method, uri: &str, body: &'static [u8]) -> Request> { - Request::builder() - .method(method) - .uri(uri) - .header("content-type", "application/json") - .header("x-extra", "value") - .body(Full::new(Bytes::from_static(body))) - .unwrap() - } - - fn rebody(req: Request) -> Request> { - let (parts, body) = req.into_parts(); - Request::from_parts(parts, Full::new(body)) - } - - #[tokio::test] - async fn round_trip() { - let key = fresh_key(); - let verifying = key.verifying_key(); - - let signed = sign(req(Method::POST, "/foo?x=1", b"hello"), &key) - .await - .unwrap(); - assert!(signed.headers().contains_key(SIG_HEADER_NAME_STR)); - - verify(rebody(signed), &verifying).await.unwrap(); - } - - #[tokio::test] - async fn rejects_double_sign() { - let key = fresh_key(); - let req = Request::builder() - .method(Method::GET) - .uri("/x") - .header(SIG_HEADER_NAME_STR, "already-here") - .body(Full::new(Bytes::new())) - .unwrap(); - let err = sign(req, &key).await.unwrap_err(); - assert!(matches!(err, Error::SignatureAlreadyPresent)); - } - - #[tokio::test] - async fn rejects_missing_sig() { - let key = fresh_key(); - let err = verify(req(Method::GET, "/x", b""), &key.verifying_key()) - .await - .unwrap_err(); - assert!(matches!(err, Error::SignatureMissing)); - } - - #[tokio::test] - async fn rejects_tampered_header() { - let key = fresh_key(); - let signed = sign(req(Method::POST, "/foo", b"hello"), &key) - .await - .unwrap(); - let mut tampered = rebody(signed); - tampered - .headers_mut() - .insert("x-extra", HeaderValue::from_static("tampered")); - let err = verify(tampered, &key.verifying_key()).await.unwrap_err(); - assert!(matches!(err, Error::SignatureInvalid)); - } - - #[tokio::test] - async fn rejects_tampered_body() { - let key = fresh_key(); - let signed = sign(req(Method::POST, "/foo", b"hello"), &key) - .await - .unwrap(); - let (parts, _body) = signed.into_parts(); - let tampered = Request::from_parts(parts, Full::new(Bytes::from_static(b"goodbye"))); - let err = verify(tampered, &key.verifying_key()).await.unwrap_err(); - assert!(matches!(err, Error::SignatureInvalid)); - } - - #[test] - fn canonical_is_order_independent() { - let mut a = HeaderMap::new(); - a.insert("x-a", HeaderValue::from_static("1")); - a.insert("x-b", HeaderValue::from_static("2")); - a.append("x-a", HeaderValue::from_static("3")); - - let mut b = HeaderMap::new(); - b.insert("x-b", HeaderValue::from_static("2")); - b.insert("x-a", HeaderValue::from_static("1")); - b.append("x-a", HeaderValue::from_static("3")); - - let target = "/path?q=v"; - let body = b"body"; - assert_eq!( - canonicalize(&Method::POST, target, &a, body), - canonicalize(&Method::POST, target, &b, body), - ); - } - - #[test] - fn canonical_excludes_unsigned_headers() { - let mut headers = HeaderMap::new(); - headers.insert("host", HeaderValue::from_static("example.com")); - headers.insert("content-length", HeaderValue::from_static("4")); - headers.insert("connection", HeaderValue::from_static("close")); - headers.insert(SIG_HEADER_NAME_STR, HeaderValue::from_static("xxx")); - headers.insert("x-keep", HeaderValue::from_static("yes")); - - let canonical = canonicalize(&Method::GET, "/", &headers, b""); - let s = std::str::from_utf8(&canonical).unwrap(); - assert!(s.contains("x-keep: yes")); - assert!(!s.contains("host:")); - assert!(!s.contains("content-length:")); - assert!(!s.contains("connection:")); - assert!(!s.contains(SIG_HEADER_NAME_STR)); - } -} diff --git a/auth-proxy/src/main.rs b/auth-proxy/src/main.rs deleted file mode 100644 index c9f9cbd..0000000 --- a/auth-proxy/src/main.rs +++ /dev/null @@ -1,188 +0,0 @@ -use std::net::{IpAddr, SocketAddr}; -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Context; -use aqora_auth_proxy::{sig_header_name, sign, SigningKey}; -use axum::{ - extract::{Request, State}, - response::{IntoResponse, Response}, - routing::any, - Router, -}; -use clap::Parser; -use http::{HeaderName, HeaderValue, StatusCode}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; - -#[derive(Parser, Debug)] -#[command(name = "auth-proxy", version, about = "Signing reverse proxy")] -struct Args { - #[arg(long, default_value = "0.0.0.0")] - host: IpAddr, - #[arg(long, default_value_t = 7777)] - port: u16, - /// Header to inject before signing, e.g. -H "Authorization: Bearer ..." - #[arg(short = 'H', long = "header", value_parser = parse_header)] - header: Vec<(HeaderName, HeaderValue)>, - /// PEM file containing a PKCS#8 Ed25519 private key - key: PathBuf, - /// Destination origin to forward to, e.g. https://aqora.io - to: url::Url, -} - -fn parse_header(s: &str) -> Result<(HeaderName, HeaderValue), String> { - let (n, v) = s.split_once(':').ok_or("expected NAME:VALUE")?; - let name = HeaderName::try_from(n.trim()).map_err(|e| e.to_string())?; - let value = HeaderValue::try_from(v.trim()).map_err(|e| e.to_string())?; - Ok((name, value)) -} - -#[derive(Clone)] -struct AppState { - key: Arc, - to: Arc, - extra_headers: Arc>, - upstream: reqwest::Client, -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - tracing_subscriber::registry() - .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) - .with(tracing_subscriber::fmt::layer()) - .init(); - - let args = Args::parse(); - - let pem_str = std::fs::read_to_string(&args.key) - .with_context(|| format!("reading key file {}", args.key.display()))?; - let key = SigningKey::from_pkcs8_pem(&pem_str).context("parsing PEM key")?; - - if args.to.cannot_be_a_base() || args.to.host_str().is_none() { - anyhow::bail!("`to` must be an absolute URL with a host, e.g. https://aqora.io"); - } - - let state = AppState { - key: Arc::new(key), - to: Arc::new(args.to), - extra_headers: Arc::new(args.header), - upstream: reqwest::Client::builder() - .redirect(reqwest::redirect::Policy::none()) - .build()?, - }; - - let app = Router::new().fallback(any(handler)).with_state(state); - - let addr = SocketAddr::from((args.host, args.port)); - let listener = tokio::net::TcpListener::bind(addr).await?; - tracing::info!("auth-proxy listening on http://{addr}"); - axum::serve(listener, app).await?; - Ok(()) -} - -enum ProxyError { - SigAlreadyPresent, - Sign(aqora_auth_proxy::Error), - InvalidUpstreamUri(String), - Upstream(reqwest::Error), -} - -impl From for ProxyError { - fn from(e: aqora_auth_proxy::Error) -> Self { - match e { - aqora_auth_proxy::Error::SignatureAlreadyPresent => Self::SigAlreadyPresent, - other => Self::Sign(other), - } - } -} - -impl IntoResponse for ProxyError { - fn into_response(self) -> Response { - match self { - Self::SigAlreadyPresent => ( - StatusCode::BAD_REQUEST, - "X-Auth-Proxy-Sig already present on incoming request\n", - ) - .into_response(), - Self::Sign(e) => { - tracing::error!(error = %e, "signing failed"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("signing error: {e}\n"), - ) - .into_response() - } - Self::InvalidUpstreamUri(msg) => ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("invalid upstream URI: {msg}\n"), - ) - .into_response(), - Self::Upstream(e) => { - tracing::warn!(error = %e, "upstream request failed"); - (StatusCode::BAD_GATEWAY, format!("upstream error: {e}\n")).into_response() - } - } - } -} - -async fn handler(State(state): State, req: Request) -> Result { - if req.headers().contains_key(sig_header_name()) { - return Err(ProxyError::SigAlreadyPresent); - } - - let (mut parts, body) = req.into_parts(); - - let mut target = (*state.to).clone(); - let pq = parts - .uri - .path_and_query() - .map(|p| p.as_str()) - .unwrap_or("/"); - let (path, query) = match pq.find('?') { - Some(idx) => (&pq[..idx], Some(&pq[idx + 1..])), - None => (pq, None), - }; - target.set_path(path); - target.set_query(query); - - parts.uri = http::Uri::try_from(target.as_str()) - .map_err(|e| ProxyError::InvalidUpstreamUri(e.to_string()))?; - - let host_header = match target.port() { - Some(p) => format!("{}:{}", target.host_str().unwrap_or(""), p), - None => target.host_str().unwrap_or("").to_string(), - }; - if let Ok(hv) = HeaderValue::try_from(host_header) { - parts.headers.insert(http::header::HOST, hv); - } - - for (name, value) in state.extra_headers.iter() { - parts.headers.insert(name.clone(), value.clone()); - } - - let mutated = Request::from_parts(parts, body); - let signed = sign(mutated, &state.key).await?; - - let (parts, body_bytes) = signed.into_parts(); - let req_for_reqwest = http::Request::from_parts(parts, reqwest::Body::from(body_bytes)); - let reqwest_req = reqwest::Request::try_from(req_for_reqwest) - .map_err(|e| ProxyError::InvalidUpstreamUri(e.to_string()))?; - - let response = state - .upstream - .execute(reqwest_req) - .await - .map_err(ProxyError::Upstream)?; - - let status = response.status(); - let version = response.version(); - let headers = response.headers().clone(); - let body = axum::body::Body::from_stream(response.bytes_stream()); - - let mut resp = Response::new(body); - *resp.status_mut() = status; - *resp.version_mut() = version; - *resp.headers_mut() = headers; - - Ok(resp) -} diff --git a/docker/Dockerfile.auth-proxy b/docker/Dockerfile.auth-proxy deleted file mode 100644 index 4b59dd6..0000000 --- a/docker/Dockerfile.auth-proxy +++ /dev/null @@ -1,20 +0,0 @@ -# syntax=docker/dockerfile:1.6 -ARG RUST_VERSION=1.94.1 - -FROM rust:${RUST_VERSION}-trixie AS build - -WORKDIR /build -COPY . . - -RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ - --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ - --mount=type=cache,target=/build/target,sharing=locked \ - cargo build --release --locked -p aqora-auth-proxy --bin auth-proxy \ - && cp /build/target/release/auth-proxy /auth-proxy - -FROM gcr.io/distroless/cc-debian13:nonroot - -COPY --from=build /auth-proxy /usr/local/bin/auth-proxy - -EXPOSE 7777 -ENTRYPOINT ["/usr/local/bin/auth-proxy"]