{"id":50830890,"url":"https://github.com/mmartinez/postern","last_synced_at":"2026-06-13T23:01:29.978Z","repository":{"id":362326578,"uuid":"1252037275","full_name":"mmartinez/postern","owner":"mmartinez","description":"Credential-brokering HTTPS proxy for AI agents — brokers 1Password \u0026 Bitwarden secrets at request time, so agents call authenticated APIs without ever holding the credentials.","archived":false,"fork":false,"pushed_at":"2026-06-03T16:53:53.000Z","size":211,"stargazers_count":2,"open_issues_count":4,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-03T17:12:50.309Z","etag":null,"topics":["1password","ai-agents","bitwarden","credential-management","devsecops","forward-proxy","golang","llm","prompt-injection","proxy","secrets-management","security"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mmartinez.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-28T06:14:00.000Z","updated_at":"2026-06-03T16:08:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mmartinez/postern","commit_stats":null,"previous_names":["mmartinez/postern"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/mmartinez/postern","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmartinez%2Fpostern","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmartinez%2Fpostern/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmartinez%2Fpostern/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmartinez%2Fpostern/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mmartinez","download_url":"https://codeload.github.com/mmartinez/postern/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmartinez%2Fpostern/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34303280,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-13T02:00:06.617Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["1password","ai-agents","bitwarden","credential-management","devsecops","forward-proxy","golang","llm","prompt-injection","proxy","secrets-management","security"],"created_at":"2026-06-13T23:01:29.098Z","updated_at":"2026-06-13T23:01:29.973Z","avatar_url":"https://github.com/mmartinez.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Postern\n\n[![CI](https://github.com/mmartinez/postern/actions/workflows/ci.yml/badge.svg)](https://github.com/mmartinez/postern/actions/workflows/ci.yml)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)\n[![1Password — supported](https://img.shields.io/badge/1Password-supported-0572EC?logo=1password\u0026logoColor=white)](docs/providers.md)\n[![Bitwarden — supported](https://img.shields.io/badge/Bitwarden-supported-175DDC?logo=bitwarden\u0026logoColor=white)](docs/providers.md)\n\n\u003e **Your AI agents call authenticated APIs without ever holding the credentials.**\n\nPostern is a credential-brokering HTTPS proxy. Agents send requests with no API\nkeys (or with harmless placeholders); postern matches the destination host\nagainst your rules, fetches the real secret from your **1Password or Bitwarden**\nvault at request time, and injects it on the way out. The agent only ever sees\nplaceholders.\n\n**Works with [1Password](https://1password.com/) (Service Accounts) and\n[Bitwarden](https://bitwarden.com/products/secrets-manager/) Secrets Manager** —\ncredential providers are [pluggable](docs/providers.md).\n\n**Why it matters:** an agent that can read a credential is a credential an\nattacker can exfiltrate through prompt injection or a compromised dependency.\nBrokering moves the secret out of the agent's reach entirely — the blast radius\nof a compromised agent no longer includes your API keys.\n\n## See it\n\nYour agent makes a normal request through the proxy, with **no `Authorization`\nheader**:\n\n```sh\ncurl -x http://localhost:1701 \\\n  https://api.anthropic.com/v1/messages \\\n  -d '{ \"model\": \"claude-sonnet-4-6\", \"messages\": [ ... ] }'\n#  ↑ no API key anywhere in the agent's environment or request\n```\n\nPostern matches `api.anthropic.com`, resolves `bw://…`/`op://…` from your vault,\ninjects the key, and forwards the now-authenticated request. The upstream sees a\nvalid call; the agent never touched the secret. On any resolver error postern\n**fails closed** with a `502` and never contacts the upstream.\n\n## How it works\n\nThe agent points `HTTPS_PROXY` at postern and trusts its local CA. For each\nrequest, postern matches the destination host against YAML-declared rules,\nresolves the matched rule's secret reference from a credential provider, injects\nthe credential, and forwards the request. The full request lifecycle and trust\nboundary are in [docs/architecture.md](docs/architecture.md).\n\nThe matched rule's secret reference (`op://…` or `bw://…`) resolves from the\nconfigured provider; adding a new provider is a single package. See\n[docs/providers.md](docs/providers.md).\n\n\u003e **Status:** early development. The proxy works end-to-end, and the release\n\u003e pipeline (checksum-verified binaries, SBOMs, and a signed multi-arch container\n\u003e image) publishes from the first tagged release (`v0.1.0`) onward. **Linux\n\u003e amd64/arm64 only** — macOS and Windows are deferred until there is demand.\n\n## Install\n\n\u003e Prebuilt binaries and the container image publish from `v0.1.0` onward. Until\n\u003e then, [build from source](#developing-postern).\n\n### Binary\n\n```sh\ncurl -fsSL https://raw.githubusercontent.com/mmartinez/postern/main/install.sh | sh\n```\n\nThe script detects your architecture, downloads the release tarball and\n`checksums.txt`, verifies the SHA-256, and installs to `/usr/local/bin/postern`.\nUse `sudo` for that default location, set `POSTERN_INSTALL_DIR` to install\nelsewhere, or `POSTERN_VERSION` to pin a release.\n\nThe SHA-256 check guards against a corrupted or truncated download, **not**\nagainst a tampered release: `checksums.txt` comes from the same source as the\ntarball, so an attacker who can rewrite the release rewrites both. For\nsupply-chain assurance, verify the keyless [cosign](https://docs.sigstore.dev/)\nsignature before trusting a download (or verify the signed container image by\ndigest):\n\n```sh\ncosign verify-blob \\\n  --bundle checksums.txt.sigstore.json \\\n  --certificate-identity-regexp '^https://github\\.com/mmartinez/postern/\\.github/workflows/release\\.yml@refs/tags/v' \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  checksums.txt\n```\n\n### Docker\n\nThe image is **`ghcr.io/mmartinez/postern`** (multi-arch linux/amd64 + arm64),\ndistroless and non-root (uid 65532). The vendor token is delivered as a mounted\nsecret, never baked into the image or its environment (the CI build asserts this\non every snapshot). See [Docker Compose](#docker-compose) for a runnable\nexample.\n\n## Quick start\n\nFrom an installed binary (or `./dist/postern` when building from source):\n\n```sh\npostern ca install        # generate a local CA and add it to your trust store\npostern config init       # write a starter ~/.postern/config.yaml\npostern token set --stdin # store your vault service-account / machine token\n\n# edit ~/.postern/config.yaml to add rules for the APIs your agent calls, then:\npostern server            # run the proxy\n\n# in the agent's shell, wire HTTPS_PROXY + CA trust in one step:\neval \"$(postern bootstrap)\"\n```\n\n`postern config validate` checks a config with line-numbered errors, and\n`postern rules list` shows the loaded rules (never the resolved credentials).\nEvery field is documented in [docs/configuration.md](docs/configuration.md).\n\nA minimal config:\n\n```yaml\ncredstores:\n  - name: vault\n    provider: 1password               # or: bitwarden\n    token:\n      source: env\n      env_var: OP_SERVICE_ACCOUNT_TOKEN\n\nproxy:\n  listen: 127.0.0.1:1701\n  cache_ttl: 5m\n\nrules:\n  - host: api.anthropic.com\n    secret_ref: op://Agents/Anthropic/api_key   # or: bw://\u003csecret-uuid\u003e\n    inject:\n      type: header\n      name: x-api-key\n      template: \"{{ CREDENTIAL }}\"\n```\n\n## Deployment\n\n### Docker Compose\n\n`docker-compose.yml` in the repo root runs the proxy with the token as a Docker\nsecret and the CA mounted read-only. It expects two files alongside it:\n\n- **`op_token`** — a `0600` file containing your vault token. Never commit it.\n- **`config.yaml`** — point the token source at the mounted secret and bind to\n  all interfaces:\n\n  ```yaml\n  token:\n    source: file\n    file: /run/secrets/op_token\n  proxy:\n    listen: 0.0.0.0:1701\n    cache_ttl: 5m\n  rules:\n    - host: api.anthropic.com\n      secret_ref: op://Agents/Anthropic/api_key\n      inject:\n        type: header\n        name: x-api-key\n        template: \"{{ CREDENTIAL }}\"\n  ```\n\npostern's CA is generated once, then read-only for its ~10-year life (the proxy\nonly reads it to sign per-host leaf certs in memory). Bootstrap it once, then\nstart the proxy:\n\n```sh\nmkdir -p postern-ca\nexport POSTERN_UID=\"$(id -u)\" POSTERN_GID=\"$(id -g)\"   # run as your uid so it can read the CA you generate\ndocker compose run --rm bootstrap                      # one-time: generate the CA into ./postern-ca\ndocker compose up -d                                   # run the proxy\n```\n\nDistribute `./postern-ca/.postern/ca.pem` to your agents, point them at the\nproxy, and have them trust the CA:\n\n```sh\nexport HTTPS_PROXY=http://localhost:1701\nexport SSL_CERT_FILE=/path/to/ca.pem                   # NODE_EXTRA_CA_CERTS for Node-based agents\n```\n\nRotating the CA (at its ~10-year expiry, or on key compromise) is just\nre-running `docker compose run --rm bootstrap` and redistributing `ca.pem`.\n\n### systemd\n\nOn a non-container Linux host, deliver the token with systemd's credential store\nso it never lands in the unit's environment or on disk unencrypted:\n\n```ini\n[Service]\nExecStart=/usr/local/bin/postern server --config /etc/postern/config.yaml\nLoadCredential=op_token:/etc/postern/op_token\n```\n\nSet `token.source: file` and `token.file:\n/run/credentials/postern.service/op_token` in `config.yaml`; systemd mounts the\ntoken read-only into the unit's private credentials directory for the lifetime of\nthe process.\n\n## Documentation\n\n- [Architecture](docs/architecture.md) — request lifecycle, trust boundary, components.\n- [Security model](docs/security.md) — fail-closed semantics, logging, threat model, key handling.\n- [Configuration](docs/configuration.md) — the full YAML reference.\n- [Providers](docs/providers.md) — the credential-vendor plugin contract (1Password, Bitwarden).\n\n## Developing postern\n\nPostern is developed **container-first**. The host needs only Docker, git, and\nthe [devcontainer CLI](https://github.com/devcontainers/cli) — no Go toolchain.\n\n```sh\ngit clone https://github.com/mmartinez/postern.git\ncd postern\ndevcontainer up --workspace-folder .   # build the container (once)\nmake shell                             # drop into it\nmake build                             # produces dist/postern\nmake ci                                # lint + test + vuln + license check (what CI runs)\n```\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow, conventions, and\nhow the git hooks work.\n\n## Security\n\nTo report a vulnerability, follow [SECURITY.md](SECURITY.md) — please do not open\na public issue. The security model (what postern defends, what it does not, and\nhow it handles keys and tokens) is documented in [docs/security.md](docs/security.md).\n\n## Trademarks\n\n1Password is a registered trademark of AgileBits Inc. Bitwarden is a trademark of\nBitwarden, Inc. Postern is not affiliated with, endorsed by, or sponsored by\nAgileBits, 1Password, or Bitwarden.\n\n## License\n\nPostern is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for\ndetails. Bundled third-party dependencies and their licenses are listed in\n[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) (generated in CI).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmmartinez%2Fpostern","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmmartinez%2Fpostern","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmmartinez%2Fpostern/lists"}