Introduction
What SecretProxy is, why it exists, and how it differs from traditional secret managers.
SecretProxy is an outbound credential-injection proxy. Instead of fetching secrets into your application at boot time, you send requests through SecretProxy with lightweight placeholders like {{API_KEY}}. The proxy resolves each placeholder, injects the real credential into the outgoing request, and forwards it to the upstream API. Your code, your runtime, your process memory — none of them ever see the secret.
How is this different from Vault or AWS Secrets Manager?
Traditional secret managers (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager) solve the storage problem: secrets are encrypted at rest, access is audited, and rotation is possible. But they still require your application to fetch the secret and hold it in memory before using it. That means secrets end up in environment variables, process memory, log files, and crash dumps.
SecretProxy solves the last-mile delivery problem. The secret never enters your application boundary. It is injected at the network edge, on its way out to the third-party API.
Architecture
Your application sends a request to secretproxy.io/api.stripe.com/v1/charges with a placeholder header. SecretProxy looks up the binding for that placeholder and target, injects the real key, and forwards the request to api.stripe.com. The response is passed back transparently.
Visit the landing page to learn more about the product.
Quick Start
Go from zero to your first proxied request in under five minutes.
Sign up at app.secretproxy.io and log in to the dashboard.
Add your first target. A target is the upstream API hostname you want to call (e.g., httpbin.org). Go to Targets → Add Target.
Create a secret. Go to Secrets → Add Secret and enter a name (e.g., HTTPBIN_TOKEN) and the actual credential value.
Create a binding. Go to Bindings → Add Binding. Select the target, choose the secret version, set the placeholder to HTTPBIN_TOKEN, and choose header injection with header name Authorization and template Bearer {{value}}.
Test with curl:
curl https://secretproxy.io/httpbin.org/get \
-H "Authorization: {{HTTPBIN_TOKEN}}"SecretProxy replaces {{HTTPBIN_TOKEN}} with Bearer <your-secret-value> and forwards the request to httpbin.org/get. The response shows the resolved Authorization header.
Update your application code. Replace the upstream URL with the SecretProxy URL and swap the real credential for a placeholder. That is the only code change needed.
// Before
const res = await fetch('https://api.acme.com/v1/data', {
headers: { 'Authorization': 'Bearer sk_live_abc123' }
});
// After (one URL change, remove the secret)
const res = await fetch('https://secretproxy.io/api.acme.com/v1/data', {
headers: { 'Authorization': '{{ACME_KEY}}' }
});GitHub Actions
Use SecretProxy in CI/CD pipelines to keep credentials out of workflow logs.
The Problem
GitHub Actions secrets are injected as environment variables, which are visible to every step in the job. Any dependency, build script, or post-processing step can read them. If a step logs the environment or a crash dump is collected, the secrets are exposed.
The Solution
Route your API calls through SecretProxy. The workflow never sees the credential — it only sends placeholders. The real secret is injected at the proxy edge.
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Call Payment API
run: |
curl https://secretproxy.io/api.acme.com/v1/deploy \
-H "Authorization: {{DEPLOY_KEY}}" \
-H "Content-Type: application/json" \
-d '{"version": "abc123"}'Whitelisting GitHub Actions IPs
GitHub Actions runners use a set of IP ranges that change over time. To whitelist them:
- Fetch the current IP ranges from GitHub's Meta API.
- Add the
actionsCIDR blocks to your SecretProxy IP whitelist via the dashboard.
Note: GitHub Actions IPs rotate frequently. For more stable access, consider using a self-hosted runner with a fixed IP, or the SecretProxy CLI which authenticates with an API token instead of IP whitelisting.
Kubernetes
Proxy traffic from K8S microservices without mounting secrets into pods.
Option 1: SaaS (Hosted)
Point your services directly at secretproxy.io. Whitelist your cluster's egress IP addresses in the SecretProxy dashboard.
# Pod using SecretProxy (SaaS)
apiVersion: v1
kind: Pod
metadata:
name: payment-service
spec:
containers:
- name: app
image: myapp:latest
env:
- name: PAYMENT_API_URL
value: "https://secretproxy.io/api.acme.com"Option 2: Enterprise (In-Cluster Proxy)
For enterprises, deploy a SecretProxy egress proxy inside your cluster. Secrets are synced from the control plane and decrypted locally. Services call secretproxy.local instead of secretproxy.io, keeping all traffic internal.
# In-cluster egress proxy deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: secretproxy-egress
spec:
replicas: 2
template:
spec:
containers:
- name: proxy
image: secretproxy/egress:latest
ports:
- containerPort: 8080Whitelisting Cluster Egress IPs
Find your cluster's NAT gateway or load balancer public IP and add it to the SecretProxy IP whitelist. For managed clusters (EKS, GKE, AKS), this is typically the NAT gateway IP of the VPC.
AWS Lambda
Remove credentials from Lambda environment variables and CloudWatch logs.
The Problem
Lambda environment variables are visible in the AWS Console, CloudTrail, and CloudWatch logs. Any IAM principal with lambda:GetFunction can read them. Secrets Manager SDKs add cold-start latency and still bring the secret into Lambda memory.
The Solution
Call through SecretProxy from your Lambda function. The secret never touches the Lambda runtime.
// Lambda function calling through SecretProxy
export const handler = async (event) => {
const response = await fetch(
'https://secretproxy.io/api.acme.com/v1/process',
{
method: 'POST',
headers: {
'Authorization': '{{PAYMENT_KEY}}',
'Content-Type': 'application/json',
},
body: JSON.stringify(event),
}
);
return response.json();
};IP Whitelisting for Lambda
If your Lambda runs inside a VPC, whitelist the NAT Gateway's Elastic IP. If it runs without a VPC, Lambda uses shared AWS IPs that are not stable — in that case, place the Lambda in a VPC with a NAT Gateway for a fixed egress IP.
Note on latency: SecretProxy runs on Cloudflare's edge network. The added latency is typically 5–20ms, which is less than the cold-start overhead of fetching from AWS Secrets Manager.
EC2 / VMs
Integrate SecretProxy with traditional server deployments.
Setup
Whitelist the instance's Elastic IP (or NAT Gateway IP if behind a VPC) in the SecretProxy dashboard. Then route your HTTP calls through the proxy.
curl
curl https://secretproxy.io/api.acme.com/v1/status \
-H "Authorization: {{ACME_KEY}}"Python (requests)
import requests
resp = requests.get(
"https://secretproxy.io/api.acme.com/v1/status",
headers={"Authorization": "{{ACME_KEY}}"}
)
print(resp.json())Node.js (fetch)
const res = await fetch('https://secretproxy.io/api.acme.com/v1/status', {
headers: { 'Authorization': '{{ACME_KEY}}' }
});
const data = await res.json();Systemd Service
For long-running daemons, no special configuration is needed. The proxy is a standard HTTPS endpoint — just point your application's API URL at SecretProxy.
Nginx Reverse Proxy
You can configure nginx to route all outbound API traffic through SecretProxy:
location /api/ {
proxy_pass https://secretproxy.io/api.acme.com/;
proxy_set_header Host secretproxy.io;
proxy_set_header Authorization "{{ACME_KEY}}";
}Docker
Use SecretProxy from Docker containers without mounting secret files or environment variables.
Docker Compose
# docker-compose.yml
services:
api:
image: myapp:latest
environment:
PAYMENT_URL: "https://secretproxy.io/api.acme.com"
# No secrets here! The app uses placeholders.Determining Your Egress IP
To whitelist your container's IP, you need to know the egress IP that SecretProxy will see. Run this from inside the container:
curl -s https://httpbin.org/ipAdd the returned IP to your SecretProxy IP whitelist in the dashboard.
Tip: In Docker Desktop (macOS/Windows), containers share the host's public IP. In production (Linux host, bridge network), the egress IP is the host machine's public IP or NAT gateway.
Local Development
Develop locally with real proxy behavior.
Whitelist Your Development IP
Go to the SecretProxy dashboard and add your current public IP to the whitelist. You can find your IP by visiting httpbin.org/ip.
SecretProxy CLI
The SecretProxy CLI provides a local proxy for development, so you can test placeholder resolution without deploying:
# Install the CLI
npm install -g @secretproxy/cli
# Login to your account
secretproxy login
# Start the local proxy (port 8787)
secretproxy dev
# Test a request through the local proxy
curl http://localhost:8787/httpbin.org/get \
-H "Authorization: {{HTTPBIN_TOKEN}}"Dashboard Proxy Test
The dashboard includes a Test page where you can construct a proxied request, inspect how placeholders are resolved, and see the upstream response — all from the browser.
Example: Local Node.js App
// Use the proxy URL from environment
const PROXY = process.env.PROXY_URL || 'https://secretproxy.io';
const res = await fetch(`${PROXY}/api.acme.com/v1/data`, {
headers: { 'Authorization': '{{ACME_KEY}}' }
});Set PROXY_URL=http://localhost:8787 in development and PROXY_URL=https://secretproxy.io in production.
Terraform
Manage SecretProxy resources as infrastructure as code.
Provider Configuration
terraform {
required_providers {
secretproxy = {
source = "ovrlab/secretproxy"
version = "~> 0.1"
}
}
}
provider "secretproxy" {
api_url = "https://api.secretproxy.io" # default
api_key = var.secretproxy_api_key
}Managing Targets
resource "secretproxy_target" "payment_api" {
name = "Payment Gateway"
base_url = "api.acme.com"
description = "Production payment API"
}
resource "secretproxy_target" "email_service" {
name = "Email Service"
base_url = "api.email-provider.com"
description = "Transactional email API"
}Managing Bindings
Bindings connect a placeholder to a secret for a specific target. The secret must already exist in the dashboard.
resource "secretproxy_binding" "payment_auth" {
target_id = secretproxy_target.payment_api.id
secret_id = 1 # Secret created in dashboard
placeholder = "PAYMENT_KEY"
injection_type = "header"
header_name = "Authorization"
header_template = "Bearer {{value}}"
}IP Whitelisting
# Whitelist a Kubernetes cluster's NAT gateway
resource "secretproxy_allowed_ip" "k8s_prod" {
ip_address = "203.0.113.10"
label = "Production K8S NAT Gateway"
scope = "tenant"
}
# Whitelist a CIDR range for staging
resource "secretproxy_allowed_ip" "staging_vpc" {
ip_address = "10.0.0.0/24"
label = "Staging VPC"
scope = "tenant"
}Data Sources
# List all targets
data "secretproxy_targets" "all" {}
# List all secrets (names only, no values)
data "secretproxy_secrets" "all" {}
output "target_names" {
value = data.secretproxy_targets.all.targets[*].name
}Full Example: Production Setup
# main.tf — SecretProxy configuration for production
variable "secretproxy_api_key" {
type = string
sensitive = true
}
provider "secretproxy" {
api_key = var.secretproxy_api_key
}
# Register all external API targets
resource "secretproxy_target" "payment" {
name = "Payment API"
base_url = "api.acme.com"
}
resource "secretproxy_target" "email" {
name = "Email Service"
base_url = "api.email-provider.com"
}
# Bindings (secrets stored in dashboard, not Terraform)
resource "secretproxy_binding" "payment_key" {
target_id = secretproxy_target.payment.id
secret_id = 1
placeholder = "PAYMENT_KEY"
injection_type = "header"
header_name = "Authorization"
header_template = "Bearer {{value}}"
}
resource "secretproxy_binding" "email_key" {
target_id = secretproxy_target.email.id
secret_id = 2
placeholder = "EMAIL_API_KEY"
injection_type = "header"
header_name = "X-API-Key"
}
# Whitelist production infrastructure
resource "secretproxy_allowed_ip" "k8s_nat" {
ip_address = "203.0.113.10"
label = "K8S NAT Gateway"
scope = "tenant"
}
resource "secretproxy_allowed_ip" "lambda_nat" {
ip_address = "198.51.100.5"
label = "Lambda VPC NAT"
scope = "service"
}What Terraform Cannot Manage
For security, the following are managed exclusively through the SecretProxy dashboard:
- Secret values — The actual API keys, tokens, and passwords. These never appear in Terraform state files.
- Secret rotation — Creating new secret versions is done through the dashboard or CLI.
- Placeholder keys (future) — The
spk_encryption keys are shown once in the dashboard and never stored externally.
How the Proxy Works
Detailed request lifecycle from client to upstream API.
Request Flow
- Client sends request to
secretproxy.io/{target_host}/{path}with placeholder tokens in headers or body. - Target lookup: The proxy extracts the first path segment as the target hostname and checks the database. If the target is not registered, it returns
404. - Placeholder detection: The proxy scans headers and body for the pattern
{{PLACEHOLDER_NAME}}using a regex match. - Binding resolution: For each placeholder, the proxy looks up the credential binding scoped to the target and tenant. If a placeholder is unknown for that target, it returns
400. - Policy enforcement: The injection type is checked. A
header-type binding found in the body (or vice versa) returns400. Templates likeBearer {{value}}are applied. - Injection: Placeholders are replaced with actual secret values. The proxy constructs a new request to the upstream target URL.
- Forward: The request is sent to the upstream API with the resolved credentials. The proxy enforces a timeout (default 30s).
- Response: The upstream response is passed back to the client transparently. SecretProxy does not modify the response body.
Error Codes
| Code | Meaning |
|---|---|
400 | Unknown placeholder, policy violation, or unresolved placeholder would leak to upstream |
403 | Client IP not whitelisted or authentication failed |
404 | Target hostname not registered |
502 | Upstream API returned an invalid response |
504 | Upstream API did not respond within the timeout |
Safety Rules
- Raw
{{TOKEN}}text is never sent to the upstream API. Unresolved placeholders cause a400error. - Body scanning only occurs for text-based content types, identity encoding, UTF-8 charset, and bodies under 1MB.
- GET and HEAD requests do not have their bodies scanned.
Placeholder Keys
How placeholders map to real credentials.
Format
Placeholders use double-curly-brace syntax: {{PLACEHOLDER_NAME}}
Names must be uppercase letters, digits, and underscores only. Examples:
{{STRIPE_KEY}}{{PAYMENT_API_TOKEN}}{{DEPLOY_KEY_V2}}
Naming Conventions
- Use SCREAMING_SNAKE_CASE.
- Include the service name for clarity:
{{STRIPE_LIVE_KEY}}not{{API_KEY}}. - Use suffixes like
_KEY,_TOKEN,_SECRETto indicate the type.
Scoping
Placeholders are scoped per target. The same placeholder name (e.g., {{API_KEY}}) can resolve to different secrets for different targets. A placeholder sent to a target it is not bound to will return 400.
One Binding Per Placeholder Per Target
Each (placeholder, target) pair maps to exactly one secret version. You cannot bind the same placeholder to multiple secrets for the same target.
Target Registration
Controlling which upstream APIs the proxy can forward to.
What Is a Target?
A target is an upstream API hostname that you register with SecretProxy. Only registered targets can receive proxied requests. Requests to unregistered hosts return 404.
Base URL Normalization
When you register a target, the base URL is normalized:
- The
https://scheme is stripped. - Trailing slashes are removed.
- The hostname is lowercased.
So https://API.Stripe.com/ becomes api.stripe.com.
Adding Targets
You can add targets via the dashboard (Targets page) or the API (POST /api/targets). Each target has a name, base URL, and optional description.
Injection Policies
How SecretProxy injects credentials into outgoing requests.
Header Injection
The most common pattern. The binding specifies injection_type: "header", a header_name (e.g., Authorization), and an optional header_template.
If a template is provided (e.g., Bearer {{value}}), the {{value}} token inside the template is replaced with the secret. The entire header value is then set on the outgoing request.
If no template is provided, the secret value replaces the placeholder directly.
Body Injection
For APIs that require credentials in the request body (e.g., JSON payloads), use injection_type: "body". The proxy scans the request body for the placeholder pattern and replaces it with the secret value.
Policy Enforcement
Injection type is strictly enforced:
- A
header-type placeholder found in the request body triggers a400error. - A
body-type placeholder found in a header triggers a400error. - This prevents accidental credential leakage across injection boundaries.
Secret Versioning
Immutable versions, controlled rotation, and rollback.
Immutable Versions
Every secret has one or more immutable versions. When you update a secret's value (via PUT /api/secrets/:id), a new version is created. Old versions are never modified or deleted.
Rotation
To rotate a credential:
- Create a new secret version with the new value.
- Update the binding to point to the new version (or use
promoteBindingVersion). - The old version remains available for rollback.
Important: Creating a new version does not automatically update bindings. This is by design — it allows controlled rollouts where you can test the new version before switching all bindings.
Rollback
To roll back, update the binding to point to the previous version. Since versions are immutable, the old value is always available.
Version Pinning
Bindings are always pinned to a specific version. This ensures that a proxy request always resolves to the exact credential you expect, even if newer versions exist.
Managing Targets
Add, edit, and remove upstream API targets from the dashboard.
Navigate to Targets in the dashboard sidebar. From here you can:
- Add a target — Provide a name (e.g., "Stripe Production"), the base URL (e.g.,
api.stripe.com), and an optional description. - Edit a target — Update the name, URL, or description.
- Delete a target — Removes the target and all associated bindings. This action cannot be undone.
Managing Secrets
Create, rotate, and inspect secrets from the dashboard.
Navigate to Secrets in the dashboard sidebar. From here you can:
- Add a secret — Enter a name (e.g.,
STRIPE_LIVE_KEY) and the value. The value is stored securely and never shown in full again. - Rotate a secret — Click "Rotate" to create a new version with a new value. The previous version is preserved.
- View versions — See all versions of a secret with masked values (only the last 4 characters are shown).
- Delete a secret — Removes the secret and all its versions. Bindings referencing this secret will stop resolving.
Managing Bindings
Connect placeholders to secrets for specific targets.
Navigate to Bindings in the dashboard sidebar. From here you can:
- Add a binding — Select a target, a secret version, a placeholder name, injection type (header or body), and optionally a header name and template.
- Edit a binding — Change the secret version (for rotation), injection type, or header configuration.
- Delete a binding — The placeholder will no longer resolve for that target.
Each binding connects one placeholder to one secret version for one target. The combination of (placeholder, target) must be unique.
IP Whitelisting
Control which IP addresses can use the proxy.
SecretProxy only accepts requests from whitelisted IP addresses. This prevents unauthorized use of your proxy bindings.
- Add IPs in the dashboard under Settings → IP Whitelist.
- Supports individual IPs and CIDR ranges (e.g.,
203.0.113.0/24). - For dynamic IPs, use the SecretProxy CLI which authenticates with an API token.
- Changes take effect immediately.
API Authentication
How to authenticate with the SecretProxy REST API.
Base URL
https://api.secretproxy.ioSession Authentication
The API uses session cookies set when you log in through the dashboard. For programmatic access, log in via the API:
curl -X POST https://api.secretproxy.io/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "you@company.com", "password": "..."}' \
-c cookies.txt
# Use the session cookie for subsequent requests
curl https://api.secretproxy.io/api/targets -b cookies.txtResponse Format
All API responses use a standard envelope:
// Success
{ "success": true, "data": { ... } }
// Error
{ "success": false, "error": "message" }
// Delete: 204 No Content (empty body)Targets API
CRUD endpoints for managing upstream API targets.
GET /api/targets
List all targets.
curl https://api.secretproxy.io/api/targets -b cookies.txtPOST /api/targets
Create a new target.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable name |
base_url | string | Yes | Upstream hostname (e.g., api.stripe.com) |
description | string | No | Optional description |
curl -X POST https://api.secretproxy.io/api/targets \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"name": "Stripe", "base_url": "api.stripe.com"}'PUT /api/targets/:id
Update a target. All fields are optional.
curl -X PUT https://api.secretproxy.io/api/targets/1 \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"description": "Production Stripe API"}'DELETE /api/targets/:id
Delete a target. Returns 204 No Content.
curl -X DELETE https://api.secretproxy.io/api/targets/1 -b cookies.txtSecrets API
CRUD endpoints for managing secrets and their versions.
GET /api/secrets
List all secrets. Values are masked (**** + last 4 characters).
curl https://api.secretproxy.io/api/secrets -b cookies.txtPOST /api/secrets
Create a new secret with its first version.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Secret name (e.g., STRIPE_LIVE_KEY) |
value | string | Yes | The secret value |
curl -X POST https://api.secretproxy.io/api/secrets \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"name": "STRIPE_LIVE_KEY", "value": "sk_live_abc123"}'PUT /api/secrets/:id
Create a new version of an existing secret.
| Field | Type | Required | Description |
|---|---|---|---|
value | string | Yes | The new secret value |
curl -X PUT https://api.secretproxy.io/api/secrets/1 \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"value": "sk_live_newvalue456"}'DELETE /api/secrets/:id
Delete a secret and all its versions. Returns 204 No Content.
curl -X DELETE https://api.secretproxy.io/api/secrets/1 -b cookies.txtBindings API
CRUD endpoints for managing credential bindings.
GET /api/bindings
List all bindings.
curl https://api.secretproxy.io/api/bindings -b cookies.txtPOST /api/bindings
Create a new binding.
| Field | Type | Required | Description |
|---|---|---|---|
external_target_id | number | Yes | Target ID |
secret_version_id | number | Yes | Secret version ID |
placeholder | string | Yes | Placeholder name (e.g., STRIPE_KEY) |
injection_type | string | Yes | header or body |
header_name | string | No | Header to inject into (for header type) |
header_template | string | No | Template with {{value}} placeholder |
curl -X POST https://api.secretproxy.io/api/bindings \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"external_target_id": 1,
"secret_version_id": 1,
"placeholder": "STRIPE_KEY",
"injection_type": "header",
"header_name": "Authorization",
"header_template": "Bearer {{value}}"
}'PUT /api/bindings/:id
Update a binding. All fields are optional.
curl -X PUT https://api.secretproxy.io/api/bindings/1 \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"secret_version_id": 2}'DELETE /api/bindings/:id
Delete a binding. Returns 204 No Content.
curl -X DELETE https://api.secretproxy.io/api/bindings/1 -b cookies.txt