Every team eventually accumulates a pile of secrets: database passwords, third-party API keys, service tokens, OAuth credentials. The standard advice is to pull them from a secrets manager at startup, stuff them into environment variables, and let the application read from there. That approach works, but it creates a class of problems that the application layer is the wrong place to solve.
The original article from blog.exe.dev argues that some of those secrets belong at the HTTP proxy layer instead. That argument is worth unpacking in detail, because the boundary between “application concern” and “infrastructure concern” is where a lot of secret management bugs live.
The Problem With App-Level Secret Injection
When an application owns its secrets end-to-end, several things become harder than they should be.
First, rotation. If your app reads a token at startup and caches it in memory, rotating that token requires restarting the process. Many teams accept this, but it means rotation events are tied to deployment events, which makes rotation less frequent and therefore riskier.
Second, sprawl. Every service that calls an external API needs its own logic for loading the credential, handling expiry, retrying on 401, and refreshing the token. That logic is usually copy-pasted across services, tested inconsistently, and subtly different in each place.
Third, auditability. When secrets live in application code, the credential usage is mixed with business logic. It is harder to answer “what called the payments API and when” without digging through application logs.
None of these problems are fatal, but they compound. The proxy layer solves all three without touching application code.
What the Proxy Layer Can Do
An HTTP proxy sitting in the request path, whether that is a sidecar container in a Kubernetes pod, a standalone reverse proxy, or a service mesh data plane, can:
- Inject an
Authorizationheader into outbound requests before they leave the network namespace - Refresh short-lived tokens transparently, retrying the upstream call with the new credential on 401
- Strip sensitive headers from responses before they reach the application
- Enforce that certain upstream endpoints are only reachable via authenticated paths
The application never sees the raw credential. It makes an unauthenticated request to a local proxy address, and the proxy adds the credential. The application does not need to know about token expiry, refresh flows, or secret manager APIs.
Concrete Pattern: Envoy with External Authorization
Envoy Proxy has a mature ext_authz filter that delegates authorization decisions to an external service. For secret injection, the pattern is slightly different: you use an ext_proc (external processing) filter or a Lua filter to rewrite outbound request headers.
A minimal Lua filter in Envoy that injects a static Bearer token looks like this:
http_filters:
- name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute
source_code:
inline_string: |
function envoy_on_request(request_handle)
request_handle:headers():replace(
"Authorization",
"Bearer " .. os.getenv("UPSTREAM_API_TOKEN")
)
end
Static tokens are the simple case. The more interesting case is dynamic tokens: OAuth2 client credentials, AWS SigV4, or short-lived Vault tokens.
For OAuth2, oauth2-proxy is the most common solution for inbound authentication, but for outbound credential injection you need something that can hold a client credentials grant and refresh it. Envoy’s OAuth2 filter covers the inbound user authentication case. For outbound machine-to-machine tokens, the pattern is usually a small sidecar that manages the token lifecycle and writes the current token to a local Unix socket or shared memory that the proxy reads from.
Vault Agent as a Proxy Companion
HashiCorp Vault Agent is designed exactly for this role. It runs alongside the application, authenticates to Vault using a platform identity (Kubernetes service account, AWS IAM role, etc.), fetches secrets, and writes them to a local file or a shared tmpfs mount.
The proxy reads the token from that file and injects it. When Vault Agent renews or rotates the secret, it updates the file. The proxy picks up the new value on the next request without a restart.
# vault-agent.hcl
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "my-service"
}
}
}
template {
contents = "{{ with secret \"secret/data/upstream-api\" }}{{ .Data.data.token }}{{ end }}"
destination = "/var/run/secrets/upstream-api-token"
command = "nginx -s reload"
}
The command field tells Vault Agent to reload nginx whenever the token changes. Combined with nginx’s auth_request module or a Lua block that reads the file, you get transparent token rotation with zero application changes.
nginx: The Practical Option
nginx is more accessible than Envoy for teams that are not already running a service mesh. The proxy_set_header directive handles static injection trivially:
location /upstream-api/ {
proxy_pass https://api.example.com/;
proxy_set_header Authorization "Bearer $upstream_api_token";
}
$upstream_api_token can be set from an environment variable using the env directive and read via Lua (ngx.var), or populated from a file using the set_by_lua_file directive in OpenResty.
For token refresh, nginx-auth-request-module lets you delegate to a small sidecar on every request:
location /upstream-api/ {
auth_request /token-provider;
auth_request_set $auth_token $upstream_http_x_auth_token;
proxy_pass https://api.example.com/;
proxy_set_header Authorization "Bearer $auth_token";
}
location = /token-provider {
internal;
proxy_pass http://localhost:8080/token;
}
The token provider is a small local service that returns the current valid token in a response header. nginx caches the auth_request response using the proxy_cache directive, so you are not hitting the token provider on every single request.
What Should Not Live at the Proxy Layer
This pattern has real limits. Database credentials should not go through an HTTP proxy, because database connections are not HTTP. For those, Vault Agent writing to a file that the application reads at connection time is still the right model, or a Vault-aware connection pool like Vault Database Secrets Engine with dynamic credentials.
Secrets that require application-level context, such as a per-user access token derived from the authenticated session, cannot be injected at the proxy layer without passing that context to the proxy via a request header, at which point you have added complexity without simplifying the application.
Application signing keys, encryption keys, and anything where the application needs to perform a cryptographic operation using the secret cannot be proxied. The application still needs direct access.
The proxy pattern works well for: API keys that are shared across all instances of a service, OAuth2 client credentials grants, static Bearer tokens for internal service-to-service calls, and webhook signing secrets used to verify inbound requests at the network edge.
The Observability Benefit
One underappreciated consequence of this pattern is that it centralizes outbound credential usage into a controlled chokepoint. Because all authenticated outbound requests flow through the proxy, you can log them uniformly: timestamp, upstream host, HTTP method, response code, and which credential was used. That data is useful for incident response, cost attribution (many APIs charge per call), and compliance auditing.
With secrets scattered across application code, producing that log requires instrumenting every service. With proxy-layer injection, the log is a configuration option on the proxy.
Fitting It Into Existing Infrastructure
For teams already on Kubernetes, the Vault Agent Injector, maintained by HashiCorp, handles the sidecar lifecycle automatically via a mutating webhook. Annotate a pod and the injector adds the Vault Agent container and the shared volume without any manifest changes beyond the annotations.
For teams using Istio or Linkerd, the service mesh data plane already handles mTLS and some credential patterns. Extending it to inject API credentials for external third-party services is a natural next step, though it requires more configuration than the basic mTLS case.
For teams without Kubernetes, a simple nginx sidecar managed by systemd, reading a token file that Vault Agent updates, covers the common cases with minimal operational overhead.
The core idea is simple: the proxy already sits in the request path. Giving it responsibility for credential injection is not adding a new component, it is giving an existing component a job it is well-positioned to do.