1Password ESO migration tooling

This doc describes the migration tooling used to move SOPS-managed Kubernetes secrets into 1Password for External Secrets Operator (ESO).

Workflow:

  1. Crawl SOPS files into metadata inventory.
  2. Push decrypted values to 1Password items with op.
  3. Generate ExternalSecret manifests that map item fields to K8s secret keys.

Goals

  • Translate SOPS-managed Kubernetes Secrets into 1Password item fields.
  • Keep plaintext out of disk by decrypting only during the push step.
  • Persist an inventory that can be reused to generate ExternalSecrets.

Requirements

  • sops installed locally.
  • 1Password account and dedicated vault (for example, homelab).
  • 1Password CLI (op) installed and authenticated.
  • OP_VAULT set in the environment (or pass --vault).

Inventory format

The crawler writes inventory.json to a chosen output directory. It is metadata-only and does not store plaintext values.

Example:

{
  "version": 1,
  "root": "kubernetes/apps",
  "vault": "homelab",
  "entries": [
    {
      "sops_path": "kubernetes/apps/default/homebox/app/secret.sops.yaml",
      "namespace": "default",
      "app": "homebox",
      "purpose": "app",
      "section": null,
      "item_name": "default.homebox",
      "fields": [
        "HBOX_MAILER_USERNAME",
        "HBOX_MAILER_PASSWORD"
      ],
      "item_id": null,
      "ks_path": "kubernetes/apps/default/homebox/ks.yaml",
      "helmrelease_path": "kubernetes/apps/default/homebox/app/helmrelease.yaml"
    }
  ]
}

Crawl workflow

Crawl a directory and build the inventory:

./scripts/onepassword/crawl.py \
  --dir kubernetes/apps \
  --output-dir ./migration-out \
  --vault homelab

Notes:

  • Namespace/app inference uses nearest ks.yaml and helmrelease.yaml.
  • op references follow op://homelab/<namespace>.<appname>/[section]/<field>.
  • Item titles should be {namespace}.{appname} and section should represent purpose when needed.
  • The crawler stores this as section metadata (null for purpose=app).
  • Separator preference between namespace and app name: ., then _, then -.
  • --output-dir defaults to --dir; OP_VAULT can provide the default vault.

Push workflow

Push inventory entries to 1Password using op:

./scripts/onepassword/push.py \
  --inventory ./migration-out/inventory.json

Notes:

  • The script decrypts each SOPS file locally and sends item templates to op via stdin.
  • Entries sharing the same item_name are merged into one 1Password item.
  • Entries with a non-null section are written into that section on the item.
  • If two source files map to the same item_name and contain different values for the same field label, that item is skipped and reported as a conflict.
  • ESO’s 1Password provider resolves by field label and ignores section names, so field labels must stay unique within an item.
  • Existing item titles prompt for apply mode: per-item, apply-all, or skip-all.
  • Item IDs are written back to the inventory unless --no-write-inventory is used.

ExternalSecret generation

Generate ExternalSecret manifests next to each SOPS file:

./scripts/onepassword/externalsecrets.py \
  --inventory ./migration-out/inventory.json

Notes:

  • Output files are derived from SOPS filename, e.g. secret-oidc.sops.yaml -> secret-oidc.externalsecret.yaml.
  • Generated manifests target ClusterSecretStore named onepassword.
  • For each key, remoteRef.key is the item title and remoteRef.property is the field label.