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:

  1. secrets.poly - block configuration with sensitive values
  2. Workspace-wide secrets - files in artifacts/secrets/ (e.g. kubeconfig.yml, id_rsa)
  3. 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):

  1. Polycrate API - fetch the encryption credential automatically from the API workspace
  2. Environment variable - WORKSPACE_ENCRYPTION_KEY (fallback if the API is not available)
  3. 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.

→ CLI reference: tools pwgen

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:

  1. All unencrypted secrets are encrypted with age
  2. Encrypted .age files are created
  3. Original files are deleted
  4. .gitignore is updated automatically
  5. The .workspace-encrypted marker 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):

  1. Polycrate API (automatic if api.enabled: true)
  2. WORKSPACE_ENCRYPTION_KEY environment variable
  3. 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:

  1. All .age files are decrypted
  2. Original files are restored
  3. .age files are deleted (locally)
  4. 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:

  1. CLI checks whether the API is configured
  2. CLI asks the API for workspace credentials of type workspace-encryption
  3. API returns an age-compatible private key
  4. 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:

  1. A .workspace-encrypted marker exists in the workspace
  2. A Polycrate command is executed
  3. 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 config
  • artifacts/secrets/kubeconfig.yml - workspace-wide file
  • artifacts/secrets/id_rsa - workspace-wide file
  • artifacts/secrets/myblock/database.pwd - block-specific file

Allowed files (✅):

  • secrets.poly.age - encrypted secrets config
  • artifacts/secrets/*.age - all encrypted workspace-wide files
  • artifacts/secrets/*/*.age - all encrypted block-specific files
  • .workspace-encrypted - encryption marker

Security best practices

✅ DO

  1. Commit .age files

    git add *.age
    git commit -m "Update encrypted secrets"
    

  2. Encrypt before pushing

    polycrate workspace encrypt
    git add .
    git commit -m "Update workspace"
    git push
    

  3. Share the key separately

  • 1Password shared vault
  • HashiCorp Vault
  • AWS Secrets Manager
  • Polycrate API (recommended)
  1. 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

  1. NEVER commit unencrypted secrets

    # WRONG:
    git add artifacts/secrets/kubeconfig.yml  # ❌ Unencrypted!
    

  2. NEVER store the key in Git

    # WRONG:
    echo "WORKSPACE_ENCRYPTION_KEY=..." > .env
    git add .env  # ❌ The key must NOT be in Git!
    

  3. NEVER bypass the hook

    # WRONG:
    git commit --no-verify  # ❌ Bypasses security checks!
    

  4. 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