CI/CD Integration

Use envctl secrets in your CI/CD pipelines without network calls to external services. Export encrypted bundles to your repository and decrypt them at build time.

How It Works

Unlike other secrets managers that require your CI to call an API, envctl uses a GitOps approach:

  1. Generate a CI keypair for your project
  2. Export secrets as an encrypted bundle
  3. Commit the bundle to your repository
  4. Decrypt at build time using the CI private key

Benefits of this approach:

  • Offline builds — No network calls during CI
  • Git history = audit trail — See when secrets changed
  • Deterministic — Same commit = same secrets
  • Fast — No API latency

Step 1: Generate CI Keypair

Run this once per project:

$ envctl ci keygen
Generated CI keypair for project "myapp"

Public key stored on project chain (committed)

CI Private Key (store in your CI platform's secrets as ENVCTL_CI_KEY):

Kz4xN2U5YzJkZmEzYjRjNWQ2ZTdmOGE5YjBjMWQyZTNmNGE1YjZjN2Q4ZTlmMGExYjJjM2Q0
ZTVmNmE3YjhjOWQwZTFmMmEzYjRjNWQ2ZTdmOGE5YjBjMWQyZTNmNGE1YjZjN2Q4ZTlmMGEx

This private key will NOT be shown again.
Store it securely in your CI platform now.

Save the private key immediately

The CI private key is shown only once. Copy it and store it in your CI platform's secrets before continuing.

Public key is stored on the team chain

The CI public key is automatically stored on the project's team chain and synced to all team members. No need to commit it to the repository.

Step 2: Export Encrypted Bundle

Export your secrets to an encrypted file:

# Export production secrets
$ envctl ci export -e prod -o .envctl/prod.enc
Passphrase:
Exported 8 variables to .envctl/prod.enc

# Export staging secrets
$ envctl ci export -e staging -o .envctl/staging.enc
Exported 6 variables to .envctl/staging.enc

The bundle is encrypted with the CI public key—only the CI private key can decrypt it. It's safe to commit:

$ git add .envctl/*.enc
$ git commit -m "Export secrets for CI"
$ git push

Any project admin can export bundles—they don't need the CI private key.

Step 3: Configure Your CI Platform

GitHub Actions

Add the CI private key to your repository secrets:

  1. Go to Settings → Secrets and variables → Actions
  2. Click "New repository secret"
  3. Name: ENVCTL_CI_KEY
  4. Value: (paste the private key)

Then use it in your workflow:

name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install envctl
        run: curl -fsSL https://raw.githubusercontent.com/uradical/envctl/main/install.sh | sh

      - name: Run tests with secrets
        env:
          ENVCTL_CI_KEY: ${{ secrets.ENVCTL_CI_KEY }}
        run: envctl ci apply -b .envctl/prod.enc -- npm test

  deploy:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Install envctl
        run: curl -fsSL https://raw.githubusercontent.com/uradical/envctl/main/install.sh | sh

      - name: Deploy with secrets
        env:
          ENVCTL_CI_KEY: ${{ secrets.ENVCTL_CI_KEY }}
        run: envctl ci apply -b .envctl/prod.enc -- ./deploy.sh

GitLab CI

Add the CI private key to your project's CI/CD variables:

  1. Go to Settings → CI/CD → Variables
  2. Add variable: ENVCTL_CI_KEY
  3. Check "Mask variable" and "Protect variable"
stages:
  - test
  - deploy

test:
  stage: test
  before_script:
    - curl -fsSL https://raw.githubusercontent.com/uradical/envctl/main/install.sh | sh
  script:
    - envctl ci apply -b .envctl/staging.enc -- npm test

deploy:
  stage: deploy
  only:
    - main
  before_script:
    - curl -fsSL https://raw.githubusercontent.com/uradical/envctl/main/install.sh | sh
  script:
    - envctl ci apply -b .envctl/prod.enc -- ./deploy.sh

CircleCI

Add ENVCTL_CI_KEY to your project environment variables in the CircleCI dashboard.

version: 2.1

jobs:
  test:
    docker:
      - image: cimg/node:18.0
    steps:
      - checkout
      - run:
          name: Install envctl
          command: curl -fsSL https://raw.githubusercontent.com/uradical/envctl/main/install.sh | sh
      - run:
          name: Run tests
          command: envctl ci apply -b .envctl/staging.enc -- npm test

  deploy:
    docker:
      - image: cimg/node:18.0
    steps:
      - checkout
      - run:
          name: Install envctl
          command: curl -fsSL https://raw.githubusercontent.com/uradical/envctl/main/install.sh | sh
      - run:
          name: Deploy
          command: envctl ci apply -b .envctl/prod.enc -- ./deploy.sh

workflows:
  build-and-deploy:
    jobs:
      - test
      - deploy:
          requires:
            - test
          filters:
            branches:
              only: main

Updating Secrets

When secrets change, re-export the bundle:

# Update a secret
$ envctl env var set -e prod NEW_API_KEY=sk_live_xyz789

# Re-export the bundle
$ envctl ci export -e prod -o .envctl/prod.enc
Exported 9 variables to .envctl/prod.enc

# Commit and push
$ git add .envctl/prod.enc
$ git commit -m "Rotate API key"
$ git push

The next CI run will automatically use the new secrets.

Git History as Audit Trail

Your git history shows when secrets changed:

$ git log --oneline .envctl/prod.enc
a1b2c3d Rotate API key after security incident
d4e5f6a Add STRIPE_WEBHOOK_SECRET
7g8h9i0 Initial CI secrets export

While you can't see the values (they're encrypted), you can see when changes were made and by whom.

Multiple Environments

Export separate bundles for each environment:

$ envctl ci export -e dev -o .envctl/dev.enc
$ envctl ci export -e staging -o .envctl/staging.enc
$ envctl ci export -e prod -o .envctl/prod.enc

Then use the appropriate bundle in each job:

test:
  script:
    - envctl ci apply -b .envctl/staging.enc -- npm test

deploy-staging:
  script:
    - envctl ci apply -b .envctl/staging.enc -- ./deploy.sh staging

deploy-prod:
  script:
    - envctl ci apply -b .envctl/prod.enc -- ./deploy.sh prod

Rotating the CI Key

If the CI private key is compromised:

  1. Generate a new keypair:
    $ envctl ci keygen --force
    ! This will invalidate existing CI bundles.
    Continue? [y/N] y
  2. Store the new private key in your CI platform
  3. Re-export all bundles:
    $ envctl ci export -e prod -o .envctl/prod.enc
    $ envctl ci export -e staging -o .envctl/staging.enc
  4. Commit and push:
    $ git add .envctl/
    $ git commit -m "Rotate CI key"
    $ git push

Security Considerations

What's Safe to Commit

File Safe to Commit? Notes
.envctl/*.enc Yes Encrypted bundles
CI public key N/A Stored on team chain, not in repo
CI private key No Store in CI secrets only

Build Log Protection

By default, envctl ci apply automatically redacts secret values from stdout and stderr, preventing accidental disclosure in build logs. If a command outputs a secret value, it appears as [REDACTED].

Use --raw to disable redaction if needed (e.g., for binary output or debugging):

envctl ci apply --raw -b .envctl/prod.enc -- ./binary-tool

Who Can Export Bundles

Any project admin can export CI bundles. They don't need the CI private key—only the public key (which is stored on the team chain and synced automatically).

Bundle Signatures

Bundles are signed by the exporter's identity. You can verify who exported a bundle:

$ envctl ci verify .envctl/prod.enc
Bundle exported by: alice (sha256:7f3a9b2c...)
Exported at: 2024-03-01 14:30:00
Variables: 8

Troubleshooting

"No CI keypair found"

No CI public key is set for this project. Run envctl ci keygen to generate one (requires the daemon to be running).

"Invalid CI key"

The ENVCTL_CI_KEY environment variable doesn't match the public key on the team chain. Check that:

  • The secret is set correctly in your CI platform
  • No extra whitespace or newlines in the key
  • The key matches the current CI public key on the project chain

"Bundle decryption failed"

The bundle was encrypted with a different CI key. Re-export the bundle:

$ envctl ci export -e prod -o .envctl/prod.enc

Related