keyctl pulls encrypted secrets from Azure Key Vault into your process's environment — without storing them on disk or in `.env` files. One small binary, one YAML file.
# commit this (no real values here) $ cat keyctl.yaml project: payments-service secrets: DATABASE_URL: database-url STRIPE_KEY: stripe-secret-key # just run it — secrets inject automatically $ keyctl run -- node server.js ✓ fetched 2 secrets for payments-service Server running on :3000
Four components, zero persisted secrets locally.
An admin runs keyctl project create payments-service. The API generates a high-entropy setup key and stores only its Argon2id hash in Azure Key Vault.
Each team member runs keyctl login --project payments-service --key <key>. The key is saved to ~/.keyctl/credentials (chmod 600) — nothing else lives locally.
keyctl.yamlA tiny config file in your repo declares which env vars map to which Key Vault secret keys. No values, just names — safe to commit.
keyctl run -- your-command fetches all declared secrets over HTTPS, injects them as env vars into the child process only, then discards them. Secrets never touch disk.
Setup keys are never stored in plaintext. The API stores only Argon2id hashes; even if Key Vault is read, no plaintext keys are exposed.
Secrets are fetched fresh for each keyctl run invocation and injected only into the child process's environment. No .env files, no temp files.
Run keyctl project rotate-key to generate a new setup key and immediately revoke the old one. Distribute the new key to your team.
The API never stores Azure credentials. It uses a system-assigned Managed Identity to talk to Key Vault — no secrets in app settings for the vault itself.
Every secret fetch, set, and delete is visible in Azure App Insights and Key Vault audit logs.
TLS 1.2+ enforced everywhere. The Function App is configured with httpsOnly: true and FTPS disabled.
# macOS / Linux curl -fsSL https://orange-sand-0b337180f.7.azurestaticapps.net/install.sh | sh # or download manually → see Download section
keyctl login \ --project payments-service \ --key <setup-key-from-admin> \ --api-url https://keyctl-api.azurewebsites.net
# Instead of: node server.js keyctl run -- node server.js # Works with any runtime keyctl run -- python manage.py runserver keyctl run -- go run ./cmd/server
# Create resource group az group create --name keyctl-rg --location eastus # Deploy Bicep (generates admin token automatically) ADMIN_TOKEN=$(openssl rand -hex 32) az deployment group create \ --resource-group keyctl-rg \ --template-file infra/main.bicep \ --parameters appName=keyctl adminToken=$ADMIN_TOKEN
GOOS=linux GOARCH=amd64 go build -o api/keyctl-api ./api cd api && func azure functionapp publish keyctl-api --no-build
KEYCTL_ADMIN_TOKEN=$ADMIN_TOKEN \ keyctl project create payments-service \ --api-url https://keyctl-api.azurewebsites.net # Output: Project "payments-service" created. Setup key (admin): 3f8a2c... This key is shown only once. Save it now.
keyctl secret set database-url "postgres://..." --project payments-service keyctl secret set stripe-secret-key "sk_live_..." --project payments-service
keyctl init --project payments-service
# Edit keyctl.yaml to map your env vars
git add keyctl.yaml && git commit -m "chore: add keyctl config"
# .github/workflows/test.yml
- name: Install keyctl
run: curl -fsSL https://orange-sand-0b337180f.7.azurestaticapps.net/install.sh | sh
- name: Run tests with secrets
env:
KEYCTL_KEY: ${{ secrets.KEYCTL_SETUP_KEY }}
KEYCTL_API_URL: https://keyctl-api.azurewebsites.net
run: keyctl run -- go test ./...
Store only the KEYCTL_SETUP_KEY in GitHub Secrets — not the individual app secrets.
Pre-built binaries for every platform, served directly from this site. See checksums.txt to verify.
curl -fsSL https://orange-sand-0b337180f.7.azurestaticapps.net/install.sh | sh