axvault
v1.13.1
Published
Remote credential storage server for axkit
Downloads
835
Maintainers
Readme
axvault
Remote credential storage server for a╳kit.
Prerequisites
- Node.js 22.19+
- PostgreSQL 14+ (local install or via Docker:
docker run -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:17-alpine) pnpm(forpnpm dlx axvault) ornpx(fornpx -y axvault)jqfor scripting against JSON API responses
If axvault is not installed globally, prefix commands with npx -y axvault (or pnpm dlx axvault).
Quick start
# Start PostgreSQL (if not already running)
docker run -d --name axvault-db -p 5432:5432 \
-e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=axvault \
postgres:17-alpine
# Generate encryption key
umask 077
printf 'AXVAULT_ENCRYPTION_KEY=' > .env
openssl rand -base64 32 >> .env
printf '\nAXVAULT_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/axvault\n' >> .env
chmod 600 .env
set -a
. ./.env
set +a
# Start server (runs migrations and creates bootstrap admin key on first startup)
npx -y axvault serveOn first startup, axvault runs database migrations and creates a bootstrap admin API key with full access. The secret is printed to stderr — save it immediately, it cannot be retrieved later.
Keep the .env file and reuse the same encryption key between restarts to avoid losing access to existing credentials.
Architecture
axvault is a server-only tool. The CLI has a single command (serve) that starts the HTTP server. All key and credential management is done through the HTTP API.
On startup, the server:
- Runs database migrations (idempotent, so they are safe to re-run on every startup)
- Creates a bootstrap admin API key if no keys exist (serialized with an advisory lock, prints secret to stderr)
- Starts listening for HTTP requests
Migration files must remain replay-safe because axvault re-runs them on every
startup instead of tracking applied versions. Use guarded SQL such as
CREATE ... IF NOT EXISTS or ALTER ... ADD COLUMN IF NOT EXISTS. If a future
schema change cannot be written safely that way, switch back to tracked
migrations rather than adding a one-shot file to this runner.
Agent Rule
Add to your CLAUDE.md or AGENTS.md:
# Rule: `axvault` Usage
Run `npx -y axvault --help` to learn available options.
Use `axvault serve` to start the credential vault server. All key and credential
management is done via the HTTP API. On first startup, a bootstrap admin API key
is created and printed to stderr.Configuration
Environment Variables
| Variable | Description | Default |
| ---------------------------- | -------------------------------------------------------------------- | ------------------------------------- |
| AXVAULT_PORT | Port to listen on | 3847 |
| AXVAULT_HOST | Host to bind to | 127.0.0.1 |
| AXVAULT_DATABASE_URL | PostgreSQL connection URL | postgresql://localhost:5432/axvault |
| AXVAULT_ENCRYPTION_KEY | Encryption key (min 32 chars, required) | — |
| AXVAULT_REFRESH_THRESHOLD | Refresh credentials expiring within this many seconds (0 to disable) | 3600 |
| AXVAULT_REFRESH_TIMEOUT_MS | Timeout for refresh operations in milliseconds | 30000 |
| AXVAULT_LOG_LEVEL | Log level (trace, debug, info, warn, error, fatal, silent) | info |
CLI Flags
The serve command accepts flags that override environment variables:
npx -y axvault serve \
--port 8080 \
--host 0.0.0.0 \
--database-url postgresql://localhost:5432/axvault \
--refresh-threshold 7200 \
--refresh-timeout 60000 \
--log-level debugSetting --refresh-threshold 0 disables automatic credential refresh.
API Keys
API keys control access to the credential API. Each key has configurable permissions:
- Read: retrieve credentials
- Write: store and delete credentials
- Grant: manage other API keys
Bootstrap Key
On first startup (when no keys exist), axvault creates a "Bootstrap Admin" key with full access (* for read, write, and grant). The secret is printed to stderr.
Managing Keys via API
All key management is done through the HTTP API:
POST /api/v1/keysaccepts either a bootstrap/admin key or a scoped grant key, as long as every requested read/write/grant entry stays within the caller'sgrantAccesslist.PATCH /api/v1/keys/:idonly works when the target key's current and resulting permissions both stay within the caller'sgrantAccesslist. In practice, scoped grant keys can only update keys that are already fully inside their scope.GET /api/v1/keys,GET /api/v1/keys/:id, andDELETE /api/v1/keys/:idrequire full grant access (grantAccess: ["*"]).
API_KEY="axv_sk_..." # Bootstrap/admin key with grantAccess ["*"]
# Create a new key
curl -X POST https://vault.example.com/api/v1/keys \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "CI Pipeline", "readAccess": ["*"], "writeAccess": ["*"], "grantAccess": []}'
# List all keys
curl https://vault.example.com/api/v1/keys \
-H "Authorization: Bearer $API_KEY"
# Get a single key
curl https://vault.example.com/api/v1/keys/k_a1b2c3d4e5f6 \
-H "Authorization: Bearer $API_KEY"
# Update key permissions
curl -X PATCH https://vault.example.com/api/v1/keys/k_a1b2c3d4e5f6 \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"readAccess": ["claude.work", "codex.ci"]}'
# Revoke a key
curl -X DELETE https://vault.example.com/api/v1/keys/k_a1b2c3d4e5f6 \
-H "Authorization: Bearer $API_KEY"Container Deployments
Container images are published automatically to registry.j4k.dev/axvault on every release (multi-arch: amd64 + arm64). To rebuild manually, run workflow_dispatch on publish-image and provide the required version input (for example 1.7.0).
Running the Container
The image uses an external UID pattern—no user is baked into the image. Always specify a non-root user with -u/--user to limit container privileges:
Security note: Without
-u/--user, the container runs as root. For Kubernetes, setrunAsUser: 1000andrunAsNonRoot: truein your SecurityContext.
# Docker (requires a PostgreSQL instance accessible from the container)
docker run -d \
--name axvault \
-p 3847:3847 \
-u 1000:1000 \
-e AXVAULT_ENCRYPTION_KEY="your-secret-key-minimum-32-chars!" \
-e AXVAULT_DATABASE_URL="postgresql://user:pass@db-host:5432/axvault" \
registry.j4k.dev/axvault:latest
# Podman
podman run -d \
--name axvault \
-p 3847:3847 \
--user 1000:1000 \
-e AXVAULT_ENCRYPTION_KEY="your-secret-key-minimum-32-chars!" \
-e AXVAULT_DATABASE_URL="postgresql://user:pass@db-host:5432/axvault" \
registry.j4k.dev/axvault:latestNote: AXVAULT_DATABASE_URL must point to an accessible PostgreSQL instance. The Containerfile default (postgresql://localhost:5432/axvault) refers to the container itself, so standalone docker run users must provide this variable. For a batteries-included setup, use docker-compose.yml which includes a PostgreSQL service.
The bootstrap admin key is created on first startup and printed to the container logs. Retrieve it with docker logs axvault or podman logs axvault.
Quadlet (systemd)
This example references axvault-db.service, which is a PostgreSQL container you must provide separately as a companion Quadlet (axvault-db.container). Alternatively, point AXVAULT_DATABASE_URL at an existing PostgreSQL instance and remove the Requires/After lines.
Create /etc/containers/systemd/axvault.container:
[Unit]
Description=axvault credential server
Requires=axvault-db.service
After=axvault-db.service
[Container]
Image=registry.j4k.dev/axvault:latest
PublishPort=3847:3847
User=1000
Group=1000
Environment=AXVAULT_ENCRYPTION_KEY=your-secret-key-minimum-32-chars!
Environment=AXVAULT_DATABASE_URL=postgresql://axvault:axvault@axvault-db:5432/axvault
[Service]
Restart=always
[Install]
WantedBy=multi-user.targetThen reload and start:
sudo systemctl daemon-reload
sudo systemctl start axvaultCredentials API
Store a Credential
curl -X PUT https://vault.example.com/api/v1/credentials/claude.prod \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{
"type": "oauth-credentials",
"provider": "anthropic",
"data": {"access_token": "...", "refresh_token": "..."},
"expiresAt": "2025-12-31T23:59:59Z"
}'Returns 201 with the stored credential's metadata:
{
"name": "claude.prod",
"createdAt": "2026-01-15T10:30:00.000Z",
"updatedAt": "2026-01-15T10:30:00.000Z"
}The type field is required and must be one of:
"oauth-credentials"— Full OAuth with refresh_token (eligible for auto-refresh)"oauth-token"— Long-lived OAuth token likeCLAUDE_CODE_OAUTH_TOKEN(static)"api-key"— API key (static)
The provider field is optional for single-provider agents. For OpenCode (multi-provider), provider is required (e.g., "anthropic", "openai", "gemini").
Optional metadata fields:
"displayName"— Human-readable label (e.g.,"Claude (Work)")"notes"— Free-form notes about the credential
These fields are stored as plaintext metadata (not encrypted) and returned in list and get responses.
List Credentials
curl https://vault.example.com/api/v1/credentials \
-H "Authorization: Bearer <api_key>"Returns metadata for all accessible credentials. Supports opt-in cursor-based pagination:
| Parameter | Description | Default |
| --------- | -------------------------------------------------------- | ----------- |
| limit | Results per page (1–1000; values above 1000 are clamped) | All results |
| cursor | Name from previous page's nextCursor | — |
Without limit, all accessible credentials are returned (backward-compatible). cursor requires limit — providing cursor without limit returns 400.
# First page
curl 'https://vault.example.com/api/v1/credentials?limit=50' \
-H "Authorization: Bearer <api_key>"
# Next page (cursor is the last name from the previous response)
curl 'https://vault.example.com/api/v1/credentials?limit=50&cursor=claude.prod' \
-H "Authorization: Bearer <api_key>"Response includes nextCursor when limit is set and more results are available:
{
"credentials": [
{
"name": "claude.staging",
"displayName": "Claude (Staging)",
"createdAt": "...",
"updatedAt": "..."
},
{ "name": "gemini.ci", "createdAt": "...", "updatedAt": "..." }
],
"nextCursor": "gemini.ci"
}Optional fields (agent, provider, displayName, notes) are included in responses only when set.
Retrieve a Credential
curl https://vault.example.com/api/v1/credentials/claude.prod \
-H "Authorization: Bearer <api_key>"Delete a Credential
Returns 204 No Content on success.
curl -X DELETE https://vault.example.com/api/v1/credentials/claude.prod \
-H "Authorization: Bearer <api_key>"Auto-Refresh
axvault automatically refreshes oauth-credentials type credentials that are near expiration when they are retrieved. This behavior is controlled by the refresh threshold setting. Only credentials containing a refresh token field are eligible for auto-refresh. Supported field names: refresh_token (standard OAuth), refreshToken (Claude), refresh (OpenCode).
Access Control Note
Auto-refresh is a server-side maintenance operation that occurs transparently during credential retrieval. Read-only API keys can trigger refresh because:
- The refresh uses the credential's own refresh token (already authorized by the token owner)
- The credential's identity and ownership remain unchanged
- Only token values and expiry timestamps are updated
- This prevents wasteful repeated refreshes and rate limit issues
This follows the pattern used by credential vaults like HashiCorp Vault, where credential maintenance is handled transparently on reads.
Response Headers
When retrieving credentials, the response may include these headers:
| Header | Value | Description |
| -------------------------- | ------ | ------------------------------------------------------------ |
| X-Axvault-Refreshed | true | Credential was successfully refreshed during this request |
| X-Axvault-Refresh-Failed | true | Refresh was attempted but failed; stale credentials returned |
When X-Axvault-Refresh-Failed is present, the response still returns HTTP 200 with the existing (potentially expired) credentials. Error details are logged to the audit log.
CI/CD Integration
With axrun (Recommended)
Use the --vault-credential flag to fetch credentials directly. Match the credential name to the agent (for example ci-claude-oauth-token for Claude and ci-codex-oauth-credentials for Codex):
- name: Run Claude Review
env:
AXVAULT: ${{ secrets.AXVAULT }}
run: |
axrun --agent claude --vault-credential ci-claude-oauth-token \
--prompt "Review this PR"With axauth (Explicit Fetch)
For more control, fetch credentials separately:
- name: Fetch credentials
run: |
axauth vault fetch --agent claude --name ci --env \
| sed 's/^export //' >> $GITHUB_ENV
env:
AXVAULT: ${{ secrets.AXVAULT }}
- name: Run Claude
run: axrun --agent claude --prompt "Hello"Required Secret
Store AXVAULT as a JSON object in your repository secrets:
{ "url": "https://vault.example.com", "apiKey": "axv_sk_..." }Alternatively, use separate environment variables:
| Variable | Description |
| ----------------- | ------------------------ |
| AXVAULT_URL | Vault server URL |
| AXVAULT_API_KEY | API key with read access |
Troubleshooting
Common Errors
| Error | Cause | Solution |
| ---------------- | ------------------------------------------ | ------------------------------------------------------------------------- |
| not-configured | Missing AXVAULT_URL or AXVAULT_API_KEY | Set both environment variables or use AXVAULT JSON |
| unauthorized | Invalid API key | Check the key via the keys API, create a new one if needed |
| forbidden | No access to credential | Update key permissions via PATCH /api/v1/keys/:id |
| not-found | Credential doesn't exist | Store credential first: axauth vault push --agent <agent> --name <name> |
| unreachable | Network issue or server down | Check vault URL, verify server is running |
Debugging
Test vault connectivity:
curl -I $AXVAULT_URL/api/v1/healthList API keys (requires grant access):
curl -H "Authorization: Bearer $AXVAULT_API_KEY" \ $AXVAULT_URL/api/v1/keysCheck credential exists:
curl -H "Authorization: Bearer $AXVAULT_API_KEY" \ $AXVAULT_URL/api/v1/credentials
License
MIT
