DevSecOps

1Password Secrets Automation in CI

1Password has quietly become a credible secrets backend for CI/CD. A walkthrough of Connect, Service Accounts, and the CLI patterns that make 1Password Secrets Automation work in a build pipeline.

Nayan Dey
Senior Security Engineer
7 min read

The first time someone told me they were using 1Password in their build pipeline I nodded politely and assumed they were copy-pasting from their personal vault. A year later I was running production deploys out of 1Password Service Accounts and wondering why nobody had told me sooner.

1Password Secrets Automation is the product line that turned 1Password from a consumer password manager into a CI-grade secrets platform. It is not trying to be Vault. It is trying to be the place your organization already puts its credentials, now accessible from a GitHub Actions runner without turning into a plaintext nightmare. For teams where 1Password is already the password manager, the friction to adopt is near zero.

The Components That Matter

1Password Secrets Automation has three parts worth understanding:

  • Connect: a self-hosted daemon that exposes a REST API and caches vaults from 1Password. You run it inside your network, usually on Kubernetes, and it holds encrypted vault data locally.
  • Service Accounts: bearer tokens that authenticate to 1Password directly, no Connect server required. These are the newer, simpler option for most CI use cases.
  • CLI (op): the same op command your developers use on their laptops. It speaks to Connect, Service Accounts, or a human 1Password account.

For CI in 2024, Service Accounts are usually the right answer. They are hosted by 1Password, require no infrastructure, and work anywhere with internet. Connect is the right answer when you need network isolation — a CI runner in a restricted VPC that cannot reach the public 1Password API, or a compliance regime that requires on-prem data.

Service Accounts in GitHub Actions

The simplest useful pattern is secret injection into a workflow using the 1Password action:

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: 1password/load-secrets-action@v2
        with:
          export-env: true
        env:
          OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
          DATABASE_URL: "op://deploy/payments-db/url"
          STRIPE_KEY: "op://deploy/stripe/secret_key"
      - run: ./deploy.sh

Two things about this workflow are worth dwelling on.

First, the secret references are paths, not values. op://deploy/payments-db/url points at the url field of the payments-db item in the deploy vault. The action resolves them at runtime. No secret value ever lives in the YAML or the repo.

Second, the OP_SERVICE_ACCOUNT_TOKEN is a GitHub Actions secret, which means 1Password bootstrap still leans on GitHub's secret store. That is fine — the token is scoped to a specific set of vaults, is revocable from the 1Password admin console, and has a full audit trail. Do not mint a single service account for the entire organization. Mint one per application or per environment, and use vault-level permissions to keep scope tight.

The CLI Pattern for Scripted Workflows

When I need more than environment variables — say, writing a temporary file with a TLS key for a deploy, or piping a secret into a tool that does not read env vars — the CLI is what I reach for:

export OP_SERVICE_ACCOUNT_TOKEN="$OP_SERVICE_ACCOUNT_TOKEN"

# Read a single field
db_url=$(op read "op://deploy/payments-db/url")

# Pull a full item as JSON for complex parsing
op item get "kafka-client" --vault=deploy --format=json > /tmp/kafka.json

# Template injection for config files
op inject -i config.template.yml -o config.yml

The op inject pattern is underrated. You keep a config template with {{ op://... }} placeholders in your repo:

database:
  url: {{ op://deploy/payments-db/url }}
  pool_size: 20

stripe:
  secret_key: {{ op://deploy/stripe/secret_key }}
  webhook_secret: {{ op://deploy/stripe/webhook_secret }}

op inject resolves the placeholders, and the resulting file exists only for the duration of the job. The template is safe to commit; the output is not. When the job ends, the file and its values are gone.

Connect: When You Need It

If your CI runners are in a restricted VPC, or if you are running self-hosted runners on hardware that does not talk to the public internet, Connect is the answer. You deploy Connect as a pair of containers (the API server and a sync container) inside your cluster, and your jobs talk to Connect instead of 1Password directly.

The deployment on Kubernetes is a Helm chart away:

helm repo add 1password https://1password.github.io/connect-helm-charts
helm install connect 1password/connect \
  --set-file connect.credentials=./1password-credentials.json \
  --set operator.create=true \
  --set operator.token.value=<access-token>

The real benefit of Connect is the 1Password Kubernetes Operator, installed as part of that chart. It introduces a OnePasswordItem CRD that syncs vault items into Kubernetes Secret objects automatically:

apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
  name: payments-db
  namespace: payments
spec:
  itemPath: "vaults/deploy/items/payments-db"

The operator syncs on a configurable interval and annotates consuming deployments for auto-rollout when the secret changes. Same model as External Secrets Operator or the Doppler Operator — different backend.

Rotation: Not a First-Class Feature

1Password does not natively rotate secrets. There is no built-in "rotate this RDS password every 30 days" button. This is a real gap compared to AWS Secrets Manager or Vault's database engine.

The honest answer is that rotation with 1Password is a workflow: a scheduled GitHub Action calls the target system's API, generates a new credential, and writes it back to 1Password:

on:
  schedule:
    - cron: '0 3 1 * *'  # Monthly at 3 AM on the 1st

jobs:
  rotate-stripe:
    runs-on: ubuntu-latest
    steps:
      - uses: 1password/load-secrets-action@v2
        with:
          export-env: true
        env:
          OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_ROTATE_TOKEN }}
          STRIPE_ADMIN_KEY: "op://rotation/stripe-admin/secret_key"
      - name: Rotate Stripe restricted key
        run: |
          new_key=$(./scripts/rotate-stripe.sh "$STRIPE_ADMIN_KEY")
          op item edit "stripe" secret_key="$new_key" --vault=deploy

This works but you own the rotation logic. For teams rotating fewer than a dozen third-party credentials, the effort is acceptable. For teams with hundreds of database credentials rotating on short cycles, 1Password is not the right backend.

The Audit Story

Every secret access through a service account shows up in the 1Password audit log with the service account identity, timestamp, and item accessed. The log is queryable via the 1Password Events API, which has Splunk, Datadog, and generic webhook consumers.

Two things to watch:

  1. The audit log records that an item was read, not which specific field. If an item has ten fields and you read one, the audit shows "item accessed" without field-level granularity.
  2. Service accounts do not currently support field-level permissions. You grant vault-level access; every field in every item in that vault is readable. Plan vault structure accordingly — do not put unrelated secrets in the same vault.

When 1Password Is the Right Call

  • Your organization already uses 1Password for human credentials and wants a single mental model.
  • Most secrets are static third-party credentials (SaaS API keys, OAuth tokens) rather than dynamically-provisioned cloud credentials.
  • You value the developer experience of human and machine access using the same op tooling.

When it is not: heavy dynamic credential needs, rotation at scale, or environments where the self-hosted footprint of Vault is already justified.

How Safeguard Helps

Safeguard inventories the op:// references across your repositories and CI configs and maps them to the 1Password vaults they read from. When a service account is over-scoped — granted to a vault it never reads — we flag it for cleanup. We also detect the reverse: repositories that hardcode secrets that already exist in 1Password vaults, a common migration leftover. And because 1Password's audit log is event-based, we correlate unusual access patterns (a new repository suddenly reading production credentials it never has before) with repository changes so the security team has context, not just an alert.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.