Skip to content

feat(secrets): add file source re-read on reload#181

Merged
mslipper merged 1 commit into
ironsh:mainfrom
drewstone:feat/file-secret-source
Jun 8, 2026
Merged

feat(secrets): add file source re-read on reload#181
mslipper merged 1 commit into
ironsh:mainfrom
drewstone:feat/file-secret-source

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

What

Adds a file secret source that reads the secret from a path on disk:

- name: secrets
  config:
    secrets:
      - source:
          type: file
          path: /etc/iron-proxy/secrets/OPENAI_API_KEY
        proxy_value: "proxy-token-123"
        match_headers: ["Authorization"]
        rules:
          - host: "api.openai.com"

The file is re-resolved on every pipeline build — boot and each POST /v1/reload — and, when ttl is set, on cache expiry. Optional ttl / failure_ttl and the shared json_key extraction apply exactly as for the existing sources.

Why

env is fixed at process exec, so a pre-warmed/long-lived proxy can never observe a secret value minted after it started. The cloud sources (aws_sm, aws_ssm, 1password*) solve rotation but require that managed infrastructure in the egress path. A file source closes the gap with the smallest surface — no new endpoint, no watcher; the existing reload just re-reads the file like it re-reads the config.

Our use case (per [chat with Matt]): one iron-proxy per sandbox, pre-warmed ahead of a customer claim. At claim we rewrite config + POST /v1/reload (works great, thanks for the pointer) — but the per-session budget-scoped key isn't known at boot. We're on bare metal, so the cloud sources aren't in play. The integrator writes the file atomically (write-temp + rename, e.g. on a tmpfs mount, 0400) and reloads; the proxy re-reads it. We understand the 0.42 control-plane is the intended longer-term home for dynamic principals/secrets — happy to align there too; this is the minimal v0.41-compatible bridge.

Behavior notes

  • Exact bytes, no trimming — the value is the literal file contents, matching how Kubernetes and Docker expose file-mounted secrets. The writer controls trailing whitespace.
  • Empty file is an error (parity with env's empty-value error).
  • Build does no I/O — only static validation (path required), consistent with the lazy-resolve contract; the read happens on first Get.
  • A missing/unreadable file surfaces a clear reading secret file %q error through the existing failure-cache path.

Tests

go test ./internal/transform/secrets/ — 187 pass. New unit tests cover happy path, exact-bytes preservation, ttl refresh picking up an atomically-rotated file, missing path / nonexistent file / empty file errors. gofmt clean, go vet clean. No integration test added — the source needs no external backend, matching env's unit-only coverage.

A file source reads the secret from a path on disk, re-resolving on every
pipeline build (boot and each POST /v1/reload) and, when ttl is set, on cache
expiry. Unlike env (fixed at process exec), this lets an integrator rotate a
running proxy's secret value by rewriting the file (atomically: write-temp +
rename) and reloading — no restart. The value is the exact file contents (no
trimming), matching how Kubernetes and Docker expose file-mounted secrets, so
the writer controls trailing whitespace. Optional ttl/failure_ttl and the
shared json_key extraction apply as they do for the other sources.
@mslipper mslipper merged commit 7e6a694 into ironsh:main Jun 8, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants