CI/CD Integration Guide
Integrate Agent Secret Store into your build pipelines. Replace dozens of CI/CD secrets with a single vault key — and get audit trails, rotation, and access control for free.
The core pattern
Store only ASS_AGENT_KEYin your CI/CD platform's secret store. Fetch all other credentials from the vault at runtime using the SDK or CLI. This reduces your CI/CD attack surface to a single credential that can be rotated independently.
GitHub Actions
Store ASS_AGENT_KEY in repository secrets (Settings → Secrets → Actions). All other credentials are fetched from the vault at runtime.
JavaScript / Node.js
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
env:
# Store only the vault key in GitHub — all others fetched at runtime
ASS_AGENT_KEY: ${{ secrets.ASS_AGENT_KEY }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
# Install Agent Secret Store SDK
- name: Install dependencies
run: npm ci
# Fetch secrets at job start
- name: Load secrets from vault
run: |
node - <<'EOF'
const { AgentVault } = require('@agentsecretstore/sdk');
const vault = new AgentVault({ agentKey: process.env.ASS_AGENT_KEY });
const fs = require('fs');
async function loadSecrets() {
const [gemini, db, stripe] = await Promise.all([
vault.getSecret('production/gemini/GEMINI_API_KEY'),
vault.getSecret('production/database/DATABASE_URL'),
vault.getSecret('production/stripe/STRIPE_SECRET_KEY'),
]);
// Write to GITHUB_ENV for downstream steps
const envFile = process.env.GITHUB_ENV;
fs.appendFileSync(envFile, `GEMINI_API_KEY=${gemini}\n`);
fs.appendFileSync(envFile, `DATABASE_URL=${db}\n`);
fs.appendFileSync(envFile, `STRIPE_SECRET_KEY=${stripe}\n`);
}
loadSecrets().catch(e => { console.error(e); process.exit(1); });
EOF
- name: Run tests
run: npm test
- name: Build
run: npm run build
- name: Deploy
run: ./scripts/deploy.shPython variant
# Python variant — fetch all at once with CLI
- name: Install CLI and fetch secrets
run: |
pip install agentsecretstore
python3 - <<'EOF'
import asyncio
import os
from agentsecretstore import AgentVault
async def main():
async with AgentVault(agent_key=os.environ["ASS_AGENT_KEY"]) as vault:
secrets = await vault.list_secrets("production")
github_env = open(os.environ["GITHUB_ENV"], "a")
for item in secrets.items:
full = await vault.get_secret(f"{item.namespace}/{item.key}")
key = item.key.upper().replace("-", "_")
github_env.write(f"{key}={full}\n")
github_env.close()
print(f"✓ Loaded {len(secrets.items)} secrets")
asyncio.run(main())
EOFGitLab CI
Add ASS_AGENT_KEY as a masked CI/CD variable (Settings → CI/CD → Variables). Use the extends keyword to apply the secret-loading pattern across jobs:
# .gitlab-ci.yml
variables:
ASS_AGENT_KEY: $ASS_AGENT_KEY # Set in GitLab CI/CD Variables (masked)
workflow:
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
stages:
- setup
- test
- build
- deploy
# Reusable secret-loading snippet
.load-secrets:
before_script:
- npm install -g @agentsecretstore/cli
- |
# Export all production secrets as shell variables
ass env export production --output .env.production
set -a
. .env.production
set +a
unit-test:
stage: test
image: node:22
extends: .load-secrets
script:
- npm ci
- npm test
build:
stage: build
image: node:22
extends: .load-secrets
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
deploy-production:
stage: deploy
image: node:22
extends: .load-secrets
only:
- main
script:
- ./scripts/deploy.sh
environment:
name: productionCircleCI
Add ASS_AGENT_KEY as a project environment variable (Project Settings → Environment Variables). Use a reusable commands block to DRY up secret loading:
# .circleci/config.yml
version: 2.1
orbs:
node: circleci/node@6
commands:
load-vault-secrets:
description: "Fetch secrets from Agent Secret Store"
steps:
- run:
name: Install Agent Secret Store SDK
command: npm install -g @agentsecretstore/cli
- run:
name: Fetch secrets from vault
command: |
# ASS_AGENT_KEY must be in CircleCI environment variables
GEMINI_KEY=$(ass secrets get production/gemini/GEMINI_API_KEY --silent)
DB_URL=$(ass secrets get production/database/DATABASE_URL --silent)
STRIPE_KEY=$(ass secrets get production/stripe/STRIPE_SECRET_KEY --silent)
echo "export GEMINI_API_KEY=$GEMINI_KEY" >> $BASH_ENV
echo "export DATABASE_URL=$DB_URL" >> $BASH_ENV
echo "export STRIPE_SECRET_KEY=$STRIPE_KEY" >> $BASH_ENV
jobs:
test:
docker:
- image: cimg/node:22.12
steps:
- checkout
- load-vault-secrets
- node/install-packages
- run: npm test
build:
docker:
- image: cimg/node:22.12
steps:
- checkout
- load-vault-secrets
- node/install-packages
- run: npm run build
- persist_to_workspace:
root: .
paths: [dist]
deploy:
docker:
- image: cimg/node:22.12
steps:
- checkout
- attach_workspace: { at: . }
- load-vault-secrets
- run: ./scripts/deploy.sh
workflows:
main:
jobs:
- test
- build:
requires: [test]
- deploy:
requires: [build]
filters:
branches:
only: mainDocker & Docker Compose
Never fetch secrets at build time
Secrets fetched during docker build are baked into image layers and visible via docker history. Always fetch secrets in the container entrypoint or application startup code — never in RUN commands.
Dockerfile
# Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# ── Runtime image ──────────────────────────────────
FROM node:22-alpine AS runtime
# Install Agent Secret Store CLI for entrypoint
RUN npm install -g @agentsecretstore/cli
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json .
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Only ASS_AGENT_KEY is passed in — all other secrets fetched at startup
ENV ASS_AGENT_KEY=""
EXPOSE 3000
ENTRYPOINT ["/entrypoint.sh"]
CMD ["node", "dist/server.js"]entrypoint.sh
#!/bin/sh
# entrypoint.sh — fetch vault secrets before starting the app
set -e
if [ -z "$ASS_AGENT_KEY" ]; then
echo "ERROR: ASS_AGENT_KEY is not set"
exit 1
fi
echo "⏳ Fetching secrets from Agent Secret Store..."
export GEMINI_API_KEY=$(ass secrets get production/gemini/GEMINI_API_KEY --silent)
export DATABASE_URL=$(ass secrets get production/database/DATABASE_URL --silent)
export STRIPE_SECRET_KEY=$(ass secrets get production/stripe/STRIPE_SECRET_KEY --silent)
export SLACK_BOT_TOKEN=$(ass secrets get production/slack/SLACK_BOT_TOKEN --silent)
echo "✓ Secrets loaded ($(ass secrets list production | wc -l) total)"
exec "$@"docker-compose.yml
# docker-compose.yml
version: '3.9'
services:
api:
build:
context: .
target: runtime
environment:
ASS_AGENT_KEY: ${ASS_AGENT_KEY}
NODE_ENV: production
ports:
- "3000:3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
worker:
build:
context: .
target: runtime
command: node dist/worker.js
environment:
ASS_AGENT_KEY: ${ASS_AGENT_KEY}
NODE_ENV: production
restart: unless-stoppedCI/CD best practices
✓ One agent per pipeline
Create a dedicated agent for CI/CD with only the scopes it needs. Don't reuse developer agent keys in pipelines.
✓ Use read-only scopes
CI/CD pipelines rarely need to write secrets. Scope to secrets:read:production/* and nothing more.
✓ Short token TTLs
If issuing tokens per job, set TTL to the job duration (5–15 minutes). Tokens that outlive the job are a risk.
✓ Mask secret values
Always use --silent with the CLI. In SDKs, never log secret values — log the path and version instead.