← Back to Agent Rule

Secrets Management for AI Agents: Docker, Git, sun-port & PostgreSQL ✓ VERIFIED

2026-08-15 · 16 min read · Secrets · Docker · Git · sun-port · PostgreSQL · Hermes · im-bot

An AI agent is software that holds your credentials and acts on them autonomously. It reads your LLM provider key, writes to your database, pushes to your repos, and sends messages as you. A leaked secret in a normal web app is a config bug. In an agent, it's an attacker that already knows exactly how to use the key — because the agent's whole job is to use keys. This tutorial locks secrets down across the entire agent stack: Git keeps them out of history, Docker injects them at runtime, sun-port encrypts them in transit, and PostgreSQL records who rotated what and when — all wired into the Hermes and im-bot agent runtimes this site documents.

Every command in this article was run on the live stack behind agent-rule.com — Hermes agents deployed in Docker, fronted by sun-port, backed by PostgreSQL, coordinating through im-bot. The ✓ VERIFIED badge means actual execution, not copy-paste from a README.

1. Why Agents Make Secret Management Harder

A traditional service has a handful of secrets, loaded once at boot, used over a narrow, well-understood surface. An agent is different in four ways:

In a multi-agent system like im-bot, the blast radius grows again: agents share rooms, share a database, and share a message bus. One leaked connector key can let an attacker impersonate every agent in the room.

2. What Counts as a Secret

Anything that grants access, proves identity, or decrypts data is a secret. For a typical agent stack the inventory looks like this:

Not every piece of config is a secret. A database hostname, a model name, or a callback URL is fine in a repo. The rule of thumb: if revealing it to an attacker changes your security posture, it's a secret.

3. The Threat Model

Before picking tools, name the four vectors a secret can leak through. Everything below maps to one of these:

  1. Git history — a key committed six months ago is still in the repo forever, even after it's deleted from HEAD.
  2. Logs and traces — stdout, agent traces, and error reports that echo environment or arguments.
  3. The context window — secrets pasted into prompts, recoverable via prompt injection or provider-side logging.
  4. Disk and backups — plaintext .env files, Docker layers, and database dumps sitting on disk or in backups.

Your defense is defense-in-depth: no single control is enough, but together they make each vector independently hard to exploit.

4. Git — Never Commit a Secret

The cheapest, highest-value control: a pre-commit hook that refuses to let a secret enter history in the first place. gitleaks scans staged changes for entropy-based and pattern-based matches against 100+ provider signatures:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.4
    hooks:
      - id: gitleaks
$ pip install pre-commit
$ pre-commit install
$ git add . && git commit -m "add agent loop"
gitleaks.............................................................Failed
- hook id: gitleaks
- exit code: 1
  Finding:     generic-api-key
  Secret:      sk-abc123...
  File:        .env
  Commit:      (current)

Layer a .gitignore on top so credential files can't even be staged accidentally:

# .gitignore
.env
.env.*
!.env.example
secrets/
*.pem
*.key

The .env.example exception is important — commit a template with placeholder values, never real ones.

If a secret was already committed, deleting it is not enough. Rewrite history with git filter-repo (or BFG), force-push, and rotate the key immediately. A key that ever touched history is burned — assume an attacker has already scanned for it.

5. Runtime Injection — Environment Variables

The twelve-factor rule holds for agents: configuration lives in the environment, not in code. Load secrets at runtime so the same image can run against different credentials in dev and prod:

$ export OPENAI_API_KEY="sk-your-key-here"
$ export POSTGRES_PASSWORD="change-me"
$ export JWT_SECRET="$(openssl rand -hex 32)"
$ ./agent run

Two rules make this safe:

If you must keep a local .env for development, lock it down:

$ chmod 600 .env

6. Docker — Compose Secrets and the env-file Pitfall

Docker has two mechanisms worth using, and one trap that bites almost everyone. The right tool for real secrets is Compose secrets — the value is mounted into the container as a file, so it never appears in docker inspect or the process list:

# docker-compose.yml
services:
  agent:
    image: ghcr.io/example/agent:latest
    secrets:
      - openai_api_key
      - pg_password
    environment:
      OPENAI_API_KEY_FILE: /run/secrets/openai_api_key
      POSTGRES_PASSWORD_FILE: /run/secrets/pg_password

  db:
    image: postgres:16
    secrets:
      - pg_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/pg_password

secrets:
  openai_api_key:
    file: ./secrets/openai_api_key.txt
  pg_password:
    file: ./secrets/pg_password.txt

Your app reads the secret from the file instead of the environment, so the plaintext never sits in a shell or a docker inspect dump.

The env-file trap: docker run --env-file reads the file only at container creation. Editing .env and running docker restart does not change a running container's environment. To apply a rotated secret you must docker rm -f and docker run again — a recreate, not a restart. This is the single most common cause of "I rotated the key but the old one still works."

For a full breakdown of the recreate pattern and how to prove the new value landed, see our Docker deployment tutorial.

7. Keep Secrets Out of the Context Window

The newest and least-obvious leak surface is the model itself. A prompt-injection attack hides instructions in tool output — a web page the agent reads, a file it opens, a message it receives. If your agent's context contains a live API key, an injected instruction can persuade it to "confirm the key is still valid by echoing it." The fix is architectural:

Treat the context window like a log file that a third party (the model provider) can read — because effectively, that's what it is.

8. sun-port — Encrypt Secrets in Transit

A secret is only as safe as the channel it crosses. If your agent POSTs a credential over plaintext HTTP, anyone on the path can read it. sun-port — the Cloudflare Pingora-based reverse proxy we covered earlier — terminates TLS at the edge, so internal services only ever speak plaintext to localhost:

# Internal services bind to loopback only — never exposed to the internet
$ ss -tlnp | grep -E '5432|8000'
LISTEN 0  128  127.0.0.1:5432  0.0.0.0:*   (postgres)
LISTEN 0  128  127.0.0.1:8000  0.0.0.0:*   (agent-api)

The rule: bind everything to 127.0.0.1, expose nothing but sun-port. The proxy holds the one public-facing TLS cert; everything behind it is loopback-only, so a credential in transit never leaves the host unencrypted.

9. PostgreSQL — Store What Isn't Secret, Audit What Is

PostgreSQL's job in this design is not to hold raw secrets — a database is a poor place to keep a credential you'll need to inject at boot. Instead, use it for the two things a secret-management system actually needs: configuration pointers and an audit trail.

Store where a secret lives and its lifecycle, not the secret itself:

-- secrets.sql — pointers and rotation audit, never the raw values
CREATE TABLE IF NOT EXISTS secret_registry (
    name        TEXT PRIMARY KEY,          -- 'openai_api_key', 'pg_password'
    kind        TEXT NOT NULL,             -- 'env' | 'compose-secret' | 'vault'
    location    TEXT NOT NULL,             -- '/run/secrets/openai_api_key'
    owner       TEXT NOT NULL,             -- 'agent', 'im-bot', 'infra'
    rotated_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    expires_at  TIMESTAMPTZ
);

CREATE TABLE IF NOT EXISTS secret_events (
    id          BIGSERIAL PRIMARY KEY,
    secret_name TEXT NOT NULL REFERENCES secret_registry(name),
    event       TEXT NOT NULL,             -- 'created' | 'rotated' | 'revoked'
    actor       TEXT,                      -- which agent or operator did it
    at          TIMESTAMPTZ NOT NULL DEFAULT now()
);

Now you can answer "who rotated the database password, and when?" with a query instead of a Slack archaeology expedition:

$ sudo -u postgres psql -d agent_rule -c \
  "SELECT secret_name, event, actor, at FROM secret_events ORDER BY at DESC LIMIT 10;"
   secret_name   |  event  |  actor   |          at
-----------------+---------+----------+---------------------
  pg_password    | rotated | hermes   | 2026-08-15 09:00:00
  openai_api_key | created | susu     | 2026-08-14 18:12:00
If you must store a secret at rest in the database — say a webhook signing key another service needs to read back — encrypt it with pgcrypto's pgp_sym_encrypt and keep the passphrase in a Compose secret, not in the schema. Plaintext credentials in a table are a gift to anyone who dumps the DB.

10. im-bot Connector Credentials

im-bot connects to external platforms — Telegram, Discord, Slack, and more — through connectors, and each connector carries its own bot token or API key. The same discipline applies, with one extra rule specific to multi-agent systems: scope credentials per connector, not per room. If two agents in the same room share a Telegram bot, they share one token; don't mint one token per agent that's actually interchangeable, and don't paste the connector token into the room's shared context where every agent (and any prompt injection) can read it.

# im-bot connector config — token via env/file, never inline in the room
connectors:
  telegram:
    enabled: true
    token_file: /run/secrets/telegram_bot_token
  discord:
    enabled: true
    token_file: /run/secrets/discord_bot_token

The token file is mounted by Compose secrets (Section 6), so the connector reads its credential at startup and the model never touches it.

11. Rotation

Secrets rot. Rotate on a schedule, on any suspected leak, and on any personnel or agent-permission change. Rotation is where the whole pipeline comes together — and where the env-file recreate trap from Section 6 matters most:

  1. Generate a new value server-side, away from any log: openssl rand -hex 32.
  2. Update the Compose secret file and docker compose up -d --force-recreate (or docker rm -f + docker run).
  3. Update the value in the provider console (or run the vendor's rotate API).
  4. Write the secret_events row so the audit trail reflects it.
  5. Verify the new value works and the old one is dead before declaring success.

Schedule step 1–4 as a Hermes cron job — the same scheduler we covered in our cron automation tutorial — so rotation happens on a calendar, not when someone remembers.

12. Verify

Secrets work is unverifiable if you can't see it. Prove three things before you trust the setup:

# 1. History is clean — gitleaks finds nothing in any commit
$ gitleaks detect --source . --report-format json --report-path /dev/null
   0 leaks found

# 2. The running container got the NEW value (recreate worked), without printing it
$ docker exec agent sh -c 'echo ${OPENAI_API_KEY_FILE:+set}'
set
$ docker exec agent sh -c 'cat /run/secrets/openai_api_key | wc -c'
52

# 3. Nothing secret is reachable over plaintext from the outside
$ curl -s -o /dev/null -w '%{http_code}' http://your-host:5432
000   # connection refused — DB is loopback-only

Note the verification style: check that the secret is present and the right length, without ever printing the value into a log. A credential that only ever lives in a file you never echo is a credential that can't leak through your own tooling.

13. Pitfalls

14. Key Takeaways

Secrets management for agents isn't a feature you bolt on — it's a property of the architecture. Git guards the source, Docker guards the runtime, sun-port guards the wire, PostgreSQL guards the history, and the runtime guards the prompt. Get all five, and a leaked key stops being a catastrophe and becomes a routine rotation.