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 App
SecretProxy Edge
Third-Party API

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.

1

Sign up at app.secretproxy.io and log in to the dashboard.

2

Add your first target. A target is the upstream API hostname you want to call (e.g., httpbin.org). Go to Targets → Add Target.

3

Create a secret. Go to Secrets → Add Secret and enter a name (e.g., HTTPBIN_TOKEN) and the actual credential value.

4

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}}.

5

Test with curl:

bash
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.

6

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.

javascript
// 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.

yaml
# .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:

  1. Fetch the current IP ranges from GitHub's Meta API.
  2. Add the actions CIDR 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.

yaml
# 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.

yaml
# 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: 8080

Whitelisting 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.

javascript
// 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

bash
curl https://secretproxy.io/api.acme.com/v1/status \
  -H "Authorization: {{ACME_KEY}}"

Python (requests)

python
import requests

resp = requests.get(
    "https://secretproxy.io/api.acme.com/v1/status",
    headers={"Authorization": "{{ACME_KEY}}"}
)
print(resp.json())

Node.js (fetch)

javascript
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:

nginx
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

yaml
# 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:

bash
curl -s https://httpbin.org/ip

Add 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:

bash
# 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

javascript
// 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.

Note: The Terraform provider manages targets, bindings, allowed IPs, and users. Secret values are never stored in Terraform state — they are managed exclusively through the dashboard.

Provider Configuration

hcl
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

hcl
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.

hcl
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

hcl
# 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

hcl
# 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

hcl
# 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

  1. Client sends request to secretproxy.io/{target_host}/{path} with placeholder tokens in headers or body.
  2. 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.
  3. Placeholder detection: The proxy scans headers and body for the pattern {{PLACEHOLDER_NAME}} using a regex match.
  4. 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.
  5. Policy enforcement: The injection type is checked. A header-type binding found in the body (or vice versa) returns 400. Templates like Bearer {{value}} are applied.
  6. Injection: Placeholders are replaced with actual secret values. The proxy constructs a new request to the upstream target URL.
  7. Forward: The request is sent to the upstream API with the resolved credentials. The proxy enforces a timeout (default 30s).
  8. Response: The upstream response is passed back to the client transparently. SecretProxy does not modify the response body.

Error Codes

CodeMeaning
400Unknown placeholder, policy violation, or unresolved placeholder would leak to upstream
403Client IP not whitelisted or authentication failed
404Target hostname not registered
502Upstream API returned an invalid response
504Upstream API did not respond within the timeout

Safety Rules

  • Raw {{TOKEN}} text is never sent to the upstream API. Unresolved placeholders cause a 400 error.
  • 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, _SECRET to 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 a 400 error.
  • A body-type placeholder found in a header triggers a 400 error.
  • 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:

  1. Create a new secret version with the new value.
  2. Update the binding to point to the new version (or use promoteBindingVersion).
  3. 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

text
https://api.secretproxy.io

Session Authentication

The API uses session cookies set when you log in through the dashboard. For programmatic access, log in via the API:

bash
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.txt

Response Format

All API responses use a standard envelope:

json
// 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.

bash
curl https://api.secretproxy.io/api/targets -b cookies.txt

POST /api/targets

Create a new target.

FieldTypeRequiredDescription
namestringYesHuman-readable name
base_urlstringYesUpstream hostname (e.g., api.stripe.com)
descriptionstringNoOptional description
bash
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.

bash
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.

bash
curl -X DELETE https://api.secretproxy.io/api/targets/1 -b cookies.txt

Secrets API

CRUD endpoints for managing secrets and their versions.

GET /api/secrets

List all secrets. Values are masked (**** + last 4 characters).

bash
curl https://api.secretproxy.io/api/secrets -b cookies.txt

POST /api/secrets

Create a new secret with its first version.

FieldTypeRequiredDescription
namestringYesSecret name (e.g., STRIPE_LIVE_KEY)
valuestringYesThe secret value
bash
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.

FieldTypeRequiredDescription
valuestringYesThe new secret value
bash
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.

bash
curl -X DELETE https://api.secretproxy.io/api/secrets/1 -b cookies.txt

Bindings API

CRUD endpoints for managing credential bindings.

GET /api/bindings

List all bindings.

bash
curl https://api.secretproxy.io/api/bindings -b cookies.txt

POST /api/bindings

Create a new binding.

FieldTypeRequiredDescription
external_target_idnumberYesTarget ID
secret_version_idnumberYesSecret version ID
placeholderstringYesPlaceholder name (e.g., STRIPE_KEY)
injection_typestringYesheader or body
header_namestringNoHeader to inject into (for header type)
header_templatestringNoTemplate with {{value}} placeholder
bash
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.

bash
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.

bash
curl -X DELETE https://api.secretproxy.io/api/bindings/1 -b cookies.txt