Workspace Encryption
Built-in age-based encryption for sensitive Polycrate workspace data: key resolution, generating keys without the Polycrate API, encrypt/decrypt/status commands, API key management, team workflow, pre-commit hook, secret access in actions, .gitignore handling and troubleshooting.
Workspace Encryption
Polycrate offers built-in encryption for sensitive workspace data, enabling teams to store and share secrets securely in Git repositories. The encryption is based on age, a modern, secure encryption tool.
Overview
The workspace encryption feature encrypts the following sensitive files:
secrets.poly- block configuration with sensitive values- Workspace-wide secrets - files in
artifacts/secrets/(e.g.kubeconfig.yml,id_rsa) - Block-specific secrets - files in
artifacts/secrets/BLOCKNAME/
Encrypted files receive the .age extension and can be committed to Git safely. The original files are automatically excluded from the commit via .gitignore.
Architecture
graph TB
User[Developer] -->|polycrate workspace encrypt| CLI[Polycrate CLI]
CLI -->|age encryption| Age[filippo.io/age]
CLI -->|checks key sources| API[Polycrate API]
CLI -->|checks key sources| ENV[WORKSPACE_ENCRYPTION_KEY]
CLI -->|checks key sources| Prompt[Interactive prompt]
API -->|Priority 1| Key[Encryption Key]
ENV -->|Priority 2| Key
Prompt -->|Priority 3| Key
Age -->|encrypts| Files[secrets.poly<br/>artifacts/secrets/**]
Files -->|creates| Encrypted[secrets.poly.age<br/>artifacts/secrets/**/*.age]
Encrypted -->|safely in| Git[Git Repository]
CLI -->|updates| Gitignore[.gitignore]
CLI -->|creates| Marker[.workspace-encrypted]
CLI -->|installs| Hook[Pre-Commit Hook]
Hook -->|blocks| Unencrypted[Unencrypted<br/>secrets]
Hook -->|allows| Encrypted
style API fill:#90EE90
style ENV fill:#FFD700
style Prompt fill:#FFA500
style Key fill:#87CEEB
style Encrypted fill:#98FB98
style Git fill:#DDA0DD
style Hook fill:#FF6B6B
style Unencrypted fill:#FFB6B6
Key resolution priority (highest → lowest):
- Polycrate API - fetch the encryption credential automatically from the API workspace
- Environment variable -
WORKSPACE_ENCRYPTION_KEY(fallback if the API is not available) - Interactive prompt - manual key input (last fallback, max. 3 attempts)
Generating an encryption key (without Polycrate API)
If the Polycrate API is not used, you must provide an age-compatible private key for WORKSPACE_ENCRYPTION_KEY yourself. This does not require installing the age CLI (age, age-keygen): the Polycrate CLI embeds the same reference implementation (filippo.io/age) as the official age tools.
polycrate tools pwgen -a age
The output contains the public key (recipient, age1...) and the private key line (AGE-SECRET-KEY-...). For polycrate workspace encrypt and polycrate workspace decrypt set:
export WORKSPACE_ENCRYPTION_KEY="AGE-SECRET-KEY-..."
Share the private key line with team members only via secure channels (password manager, confidential handover, …). The public key is sufficient for scenarios in which third parties should only encrypt; the typical Polycrate workflow uses the private key via the environment variable.
Workspace directory layout with secrets
workspace/
├── workspace.poly # Public workspace config
├── secrets.poly # ❌ Unencrypted - NOT in Git
├── secrets.poly.age # ✅ Encrypted - IN GIT
├── .workspace-encrypted # ✅ Encryption marker - IN GIT
├── .gitignore # Managed automatically
│
└── artifacts/
└── secrets/ # Secret artifacts root
│
├── kubeconfig.yml # ❌ Workspace-wide, unencrypted
├── kubeconfig.yml.age # ✅ Workspace-wide, encrypted
├── id_rsa # ❌ SSH private key
├── id_rsa.age # ✅ SSH private key (encrypted)
├── id_rsa.pub # ❌ SSH public key
├── id_rsa.pub.age # ✅ SSH public key (encrypted)
│
└── myblock/ # Block-specific secrets
├── secret.txt # ❌ Unencrypted
├── secret.txt.age # ✅ Encrypted
├── database.pwd # ❌ Unencrypted
└── database.pwd.age # ✅ Encrypted
Rule: only .age files and .workspace-encrypted may be committed to Git.
Getting started
1. Initialize the workspace
mkdir my-workspace
cd my-workspace
polycrate workspace init --with-name my-workspace --with-organization my-org
2. Create secrets
secrets.poly - block configuration
The secrets.poly file contains sensitive block configuration values that override the values from workspace.poly:
# secrets.poly - sensitive block config overrides
blocks:
- name: my-database-block
config:
# Public values can be overridden
database_port: 5432
# Secret values are added
database_password: "supersecret123"
admin_password: "verysecure456"
- name: my-api-block
config:
api_token: "tok_abc123xyz789"
ssh_private_key: |
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
Merge priority:
block.poly < workspace.poly < secrets.poly
secrets.poly has the highest priority and overrides all other configuration values.
Workspace-wide secrets
# Kubernetes configuration
cat > artifacts/secrets/kubeconfig.yml << EOF
apiVersion: v1
kind: Config
clusters:
- name: production-cluster
cluster:
server: https://k8s.example.com
certificate-authority-data: LS0t...
EOF
# SSH keys (if not already present)
ssh-keygen -t rsa -b 4096 -f artifacts/secrets/id_rsa -N ""
Block-specific secrets
# Secrets for a specific block
mkdir -p artifacts/secrets/my-database-block
echo "postgres://user:pass@host/db" > artifacts/secrets/my-database-block/connection-string.txt
echo "backup-secret-key-123" > artifacts/secrets/my-database-block/backup-key.txt
3. Encrypt
# Fetch the key from the Polycrate API (automatic if configured)
# OR without API: generate a key, e.g. polycrate tools pwgen -a age
# OR: set an existing key as environment variable
export WORKSPACE_ENCRYPTION_KEY="AGE-SECRET-KEY-..."
# Encrypt all secrets
polycrate workspace encrypt
Output:
Found 5 files to encrypt
Encrypted: secrets.poly -> secrets.poly.age
Encrypted: artifacts/secrets/kubeconfig.yml -> artifacts/secrets/kubeconfig.yml.age
Encrypted: artifacts/secrets/id_rsa -> artifacts/secrets/id_rsa.age
Encrypted: artifacts/secrets/id_rsa.pub -> artifacts/secrets/id_rsa.pub.age
Encrypted: artifacts/secrets/my-database-block/connection-string.txt -> artifacts/secrets/my-database-block/connection-string.txt.age
Workspace encryption completed successfully
What happens during encryption:
- All unencrypted secrets are encrypted with age
- Encrypted
.agefiles are created - Original files are deleted
.gitignoreis updated automatically- The
.workspace-encryptedmarker is created/updated
4. Commit to Git
git add .
git commit -m "Add workspace with encrypted secrets"
git push
Git now contains:
- ✅
secrets.poly.age - ✅
artifacts/secrets/*.age - ✅
.workspace-encrypted - ✅
.gitignore - ❌ NO unencrypted secrets!
CLI commands
polycrate workspace encrypt
Encrypts all sensitive workspace files.
polycrate workspace encrypt
Key sources (in priority order):
- Polycrate API (automatic if
api.enabled: true) WORKSPACE_ENCRYPTION_KEYenvironment variable- Interactive prompt (if no key is found)
Single-file mode (since CLI 0.49.0): with --file / -f only one file is encrypted — without the full workspace tree. Output via --output / -o or stdout.
Examples:
# With API key (automatic)
polycrate workspace encrypt
# With environment variable
export WORKSPACE_ENCRYPTION_KEY="AGE-SECRET-KEY-..."
polycrate workspace encrypt
# With interactive prompt (no ENV set)
polycrate workspace encrypt
# > Enter workspace key (attempt 1 of 3): ****
# Single file
polycrate workspace encrypt --file ./payload.yml -o ./payload.yml.age
polycrate workspace decrypt
Decrypts all encrypted workspace files.
polycrate workspace decrypt
# Single file (since CLI 0.49.0)
polycrate workspace decrypt --file ./payload.yml.age -o ./payload.yml
What happens during decryption:
- All
.agefiles are decrypted - Original files are restored
.agefiles are deleted (locally)- The workspace is now usable locally
Example:
export WORKSPACE_ENCRYPTION_KEY="AGE-SECRET-KEY-..."
polycrate workspace decrypt
Output:
Found 5 encrypted files to decrypt
Decrypted: secrets.poly.age -> secrets.poly
Decrypted: artifacts/secrets/kubeconfig.yml.age -> artifacts/secrets/kubeconfig.yml
Decrypted: artifacts/secrets/id_rsa.age -> artifacts/secrets/id_rsa
Decrypted: artifacts/secrets/id_rsa.pub.age -> artifacts/secrets/id_rsa.pub
Decrypted: artifacts/secrets/my-database-block/connection-string.txt.age -> artifacts/secrets/my-database-block/connection-string.txt
Workspace decryption completed successfully
polycrate workspace status
Shows the encryption status of the workspace.
polycrate workspace status
Output:
Workspace Encryption Status
============================
Workspace: https://polycrate.example.com/workspaces/3fc897cd-...
UUID: 3fc897cd-f557-4f48-b5a3-6aa0c5e49fe6
🔑 Key Status: ✅ Key available
Key ID: age12e956sgdpsxh...
Source: Polycrate API (priority 1)
📁 File Status:
Encrypted files:
✅ secrets.poly.age
✅ artifacts/secrets/kubeconfig.yml.age
✅ artifacts/secrets/id_rsa.age
✅ artifacts/secrets/my-database-block/connection-string.txt.age
🔒 Security Status:
.gitignore: ✅ Properly configured
Git repo: ✅ Detected
Branch: main
Commit: 923aaccb (Add workspace config)
Remote: ↑2 ahead
Changes: 3 staged, 0 unstaged, 0 untracked
💡 Recommendations:
- Workspace is properly encrypted and ready to commit
Key source display when no key is found:
🔑 Key Status: ❌ No key found
Key sources checked (in priority order):
1. Polycrate API: ✅ Encryption credential found
- ID: cred-abc123
- Kind: workspace-encryption
- State: READY
2. Environment variable WORKSPACE_ENCRYPTION_KEY: ❌ Not set
3. Interactive prompt: Available on demand
Polycrate API integration
The Polycrate API provides central key management for workspace encryption and is the recommended solution for teams.
How it works
When the API integration is enabled (api.enabled: true), the Polycrate CLI automatically fetches the workspace encryption key from the API. This has the highest priority in key resolution and overrides local environment variables.
Key resolution flow with API:
- CLI checks whether the API is configured
- CLI asks the API for workspace credentials of type
workspace-encryption - API returns an age-compatible private key
- CLI automatically uses the key for encryption/decryption
Credential storage:
The encryption key is stored as a workspace credential in the API. The API manages:
- Private key (
ssh_private_key): age secret key for encryption/decryption - Public key (
ssh_public_key): age public key (derived from the private key) - State: READY, PENDING or ERROR
- Metadata: workspace assignment, creation date, etc.
Advantages of the API integration:
- ✅ Automatic key retrieval - no manual key management required
- ✅ Central team management - one key for all team members
- ✅ Workspace-specific - every workspace has its own key
- ✅ State tracking - monitoring of the credential status
- ✅ Audit logging - traceability of all key accesses
- ✅ Key rotation - central update without local changes
For details on API configuration, authentication and credential management see the Polycrate API documentation.
Team workflow
Developer A (initial setup)
# 1. Create the workspace
git clone <repo>
cd workspace
polycrate workspace init --with-name my-workspace --with-organization my-org
# 2. Create secrets
cat > secrets.poly << EOF
blocks:
- name: my-database-block
config:
database_password: "secret123"
EOF
mkdir -p artifacts/secrets
echo "kubeconfig-data" > artifacts/secrets/kubeconfig.yml
# 3. Configure the Polycrate API (optional but recommended)
cat > ~/.polycrate/polycrate.yml << EOF
api:
enabled: true
url: https://api.polycrate.example.com
api_key: your-api-key-here
EOF
# 4. Encrypt (key is fetched automatically from the API)
polycrate workspace encrypt
# 5. Commit to Git
git add .
git commit -m "Add workspace with encrypted secrets"
git push
Developer B (team member)
# 1. Clone the repository
git clone <repo>
cd workspace
# Structure after clone:
# ✅ workspace.poly
# ✅ .workspace-encrypted
# ✅ secrets.poly.age
# ✅ artifacts/secrets/*.age
# ❌ NO unencrypted secrets
# 2. Configure API access
cat > ~/.polycrate/polycrate.yml << EOF
api:
enabled: true
url: https://api.polycrate.example.com
api_key: your-api-key-here
EOF
# 3. Check workspace status
polycrate workspace status
# Output: pre-commit hook installed automatically
# 4. Decrypt (key is fetched automatically from the API)
polycrate workspace decrypt
# 5. The workspace is now usable
polycrate run my-block install
Without API (manual key management):
Developer A shares the encryption key via a secure channel (1Password, Signal, etc.):
# Developer B sets the key
export WORKSPACE_ENCRYPTION_KEY="AGE-SECRET-KEY-..."
# Decrypt
polycrate workspace decrypt
Pre-commit hook
Polycrate automatically installs a pre-commit hook that prevents unencrypted secrets from being committed.
Automatic installation
The hook is installed automatically when:
- A
.workspace-encryptedmarker exists in the workspace - A Polycrate command is executed
- The directory is a Git repository
Hook behavior
Blocks unencrypted secrets:
# Try to commit an unencrypted secrets.poly
git add secrets.poly
git commit -m "Add secrets"
# Output:
# 🔒 Running Polycrate security checks...
#
# ❌ FATAL: Cannot commit - UNENCRYPTED secrets detected:
# - secrets.poly
#
# Secrets must be encrypted before committing!
#
# Fix:
# 1. Encrypt: polycrate workspace encrypt
# 2. This creates .age files (encrypted, safe to commit)
# 3. Original files are deleted automatically
# 4. Commit the .age files
Allows encrypted .age files:
# Encrypt first
polycrate workspace encrypt
# Commit .age files
git add secrets.poly.age
git commit -m "Add encrypted secrets"
# Output:
# 🔒 Running Polycrate security checks...
# ✅ Security checks passed
# [main abc1234] Add encrypted secrets
Disabling the hook (not recommended)
# Emergencies only - do NOT use in production!
git commit --no-verify -m "..."
Secret access in actions
In Ansible playbooks
Secrets are available via the workspace snapshot as file paths:
# Ansible playbook - workspace-wide secrets
- name: Use workspace kubeconfig
shell: kubectl --kubeconfig={{ workspace.secrets['kubeconfig.yml'] }} get pods
- name: Use workspace SSH key
shell: ssh -i {{ workspace.secrets['id_rsa'] }} user@host
# Block-specific secrets
- name: Use block database password
shell: |
DB_PASSWORD=$(cat {{ block.secrets['database.pwd'] }})
psql -h localhost -U user -d mydb
- name: Use block API token
uri:
url: https://api.example.com/endpoint
headers:
Authorization: "Bearer $(cat {{ block.secrets['api-token.txt'] }})"
Secret paths
The workspace snapshot contains:
workspace:
secrets:
kubeconfig.yml: /workspace/artifacts/secrets/kubeconfig.yml
id_rsa: /workspace/artifacts/secrets/id_rsa
id_rsa.pub: /workspace/artifacts/secrets/id_rsa.pub
block:
secrets:
database.pwd: /workspace/artifacts/secrets/myblock/database.pwd
api-token.txt: /workspace/artifacts/secrets/myblock/api-token.txt
Manual encryption with the age CLI
All files encrypted by Polycrate use the standard age format and are 100% compatible with the age CLI tool.
Installing the age CLI
# macOS
brew install age
# Linux (Ubuntu/Debian)
sudo apt install age
# Linux (Arch)
sudo pacman -S age
Generating a key
age-keygen > workspace-key.txt
cat workspace-key.txt
Output:
# created: 2025-01-30T10:30:00Z
# public key: age1h6gqcdfckepddxa4r545w46z4n3pglj70p0defyf4mp9ja9suptsqwkk89
AGE-SECRET-KEY-1Q2LKW207HDD7MT30NQXRPSFYU0782R8V4XRHC6NJ4TLUPMLWRNPSHC83W0
Manual encryption
# Set the secret key
export SECRET_KEY="AGE-SECRET-KEY-..."
# Encrypt a single file
echo "$SECRET_KEY" | age --encrypt -i - secrets.poly > secrets.poly.age
# Extract the public key
PUBLIC_KEY=$(echo "$SECRET_KEY" | age-keygen -y)
# Encrypt with the public key
age --encrypt -r "$PUBLIC_KEY" secrets.poly > secrets.poly.age
Manual decryption
# Decrypt a single file
echo "$SECRET_KEY" | age --decrypt -i - secrets.poly.age > secrets.poly
# Decrypt all .age files
for file in artifacts/secrets/**/*.age; do
echo "$SECRET_KEY" | age --decrypt -i - "$file" > "${file%.age}"
done
Advantages of age compatibility:
- ✅ Emergency decryption without Polycrate
- ✅ Integration into CI/CD with standard tools
- ✅ Manual key management possible
- ✅ No vendor lock-in
.gitignore management
Polycrate automatically updates .gitignore to ensure that only encrypted files are committed.
Automatically added entries
# Polycrate secrets (unencrypted)
secrets.poly # Unencrypted secrets.poly
artifacts/secrets/* # Workspace-wide secrets (files)
!artifacts/secrets/*/ # Block directories allowed
!artifacts/secrets/*.age # Workspace .age files allowed
artifacts/secrets/*/* # Block-specific secrets (files)
!artifacts/secrets/*/*.age # Block .age files allowed
!.workspace-encrypted # Encryption marker MUST be committed
# Legacy files (for old workspaces)
id_rsa # Legacy SSH key in root
id_rsa.pub # Legacy SSH pub key in root
/kubeconfig.yml # Legacy kubeconfig in root
/kubeconfig.yaml # Legacy kubeconfig in root
Pattern matching logic
Ignored files (❌):
secrets.poly- unencrypted secrets configartifacts/secrets/kubeconfig.yml- workspace-wide fileartifacts/secrets/id_rsa- workspace-wide fileartifacts/secrets/myblock/database.pwd- block-specific file
Allowed files (✅):
secrets.poly.age- encrypted secrets configartifacts/secrets/*.age- all encrypted workspace-wide filesartifacts/secrets/*/*.age- all encrypted block-specific files.workspace-encrypted- encryption marker
Security best practices
✅ DO
-
Commit .age files
git add *.age git commit -m "Update encrypted secrets" -
Encrypt before pushing
polycrate workspace encrypt git add . git commit -m "Update workspace" git push -
Share the key separately
- 1Password shared vault
- HashiCorp Vault
- AWS Secrets Manager
- Polycrate API (recommended)
- Use the API for automatic key management
# ~/.polycrate/polycrate.yml api: enabled: true url: https://api.polycrate.example.com api_key: your-api-key
❌ DON'T
-
NEVER commit unencrypted secrets
# WRONG: git add artifacts/secrets/kubeconfig.yml # ❌ Unencrypted! -
NEVER store the key in Git
# WRONG: echo "WORKSPACE_ENCRYPTION_KEY=..." > .env git add .env # ❌ The key must NOT be in Git! -
NEVER bypass the hook
# WRONG: git commit --no-verify # ❌ Bypasses security checks! -
NEVER push unencrypted
# WRONG: polycrate workspace decrypt git add secrets.poly # ❌ Unencrypted! git push # ❌ The pre-commit hook blocks this!
Error handling
"No workspace key found"
Problem: no encryption key available
Solution:
# Option 1: configure the API
cat > ~/.polycrate/polycrate.yml << EOF
api:
enabled: true
url: https://api.polycrate.example.com
api_key: your-api-key
EOF
# Option 2: set the environment variable
export WORKSPACE_ENCRYPTION_KEY="AGE-SECRET-KEY-..."
# Option 3: interactive prompt (automatic)
polycrate workspace decrypt
# > Enter workspace key: ****
"FATAL: Encrypted files found"
Problem: the workspace contains encrypted files and cannot be loaded
Solution:
# Decrypt first
polycrate workspace decrypt
# Then perform workspace operations
polycrate run my-block install
"Key does not match encrypted file"
Problem: wrong encryption key used
Solution:
# Check the API configuration
polycrate workspace status
# Or use the correct key
export WORKSPACE_ENCRYPTION_KEY="AGE-SECRET-KEY-CORRECT-KEY-HERE"
polycrate workspace decrypt
"UNENCRYPTED secrets detected" (pre-commit hook)
Problem: attempt to commit unencrypted secrets
Solution:
# Encrypt first
polycrate workspace encrypt
# Commit .age files
git add .
git commit -m "Update encrypted secrets"
Migrating legacy workspaces
If your workspace has SSH keys or kubeconfigs directly in the root directory, Polycrate migrates them automatically to artifacts/secrets/:
Automatic migration
# Legacy structure (old):
workspace/
├── id_rsa # In root
├── id_rsa.pub # In root
├── kubeconfig.yml # In root
# Modern structure (after migration):
workspace/
└── artifacts/secrets/
├── id_rsa # Migrated
├── id_rsa.pub # Migrated
└── kubeconfig.yml # Migrated
Triggering the migration:
# Simply encrypt - the migration happens automatically
polycrate workspace encrypt
Advanced usage
Custom key generation
# Recommended: generate the key with Polycrate (no age CLI required)
polycrate tools pwgen -a age
export WORKSPACE_ENCRYPTION_KEY="AGE-SECRET-KEY-..."
# Alternatively: age CLI
age-keygen -o workspace-key.txt
PRIVATE_KEY=$(grep "AGE-SECRET-KEY" workspace-key.txt)
export WORKSPACE_ENCRYPTION_KEY="$PRIVATE_KEY"
polycrate workspace encrypt
CI/CD integration
# .github/workflows/deploy.yml
name: Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install age
run: |
sudo apt-get update
sudo apt-get install -y age
- name: Decrypt workspace
env:
WORKSPACE_ENCRYPTION_KEY: ${{ secrets.WORKSPACE_ENCRYPTION_KEY }}
run: |
echo "$WORKSPACE_ENCRYPTION_KEY" | age --decrypt -i - secrets.poly.age > secrets.poly
for file in artifacts/secrets/**/*.age; do
echo "$WORKSPACE_ENCRYPTION_KEY" | age --decrypt -i - "$file" > "${file%.age}"
done
- name: Deploy
run: polycrate run my-block deploy
Key rotation
# 1. Decrypt with the old key
export WORKSPACE_ENCRYPTION_KEY="$OLD_KEY"
polycrate workspace decrypt
# 2. Encrypt with the new key
export WORKSPACE_ENCRYPTION_KEY="$NEW_KEY"
polycrate workspace encrypt
# 3. Commit
git add .
git commit -m "Rotate encryption key"
git push
See also
- Workspaces - Workspace basics
- CLI reference - Complete CLI documentation
- Best practices - Best practices for Polycrate
- Configuration - Workspace configuration
- age-encryption.org - age encryption tool