> ## Content Index
> Fetch the complete content index at: https://neumachen.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# How I Use 1Password with External Secrets Operator, and the Boundary I Got Wrong
- URL: https://neumachen.dev/articles/1password-external-secrets-operator-bootstrap-boundary/
- Published: 2026-10-17T16:27:17.000Z
- Updated: 2026-10-17T16:57:42.000Z
- Description: I already trusted 1Password with my family's passwords. External Secrets Operator gave it a second job on my home server, and exposed a bootstrap boundary I still need to fix.
- Author: Kareem Hepburn
- Tags: 1Password, Kubernetes, External Secrets Operator, Homelab, GitOps, #nc-article-1password-external-secrets-operator-bootstrap-boundary

I was using 1Password long before I needed it to deliver secrets into a Kubernetes cluster.

My family account is where I keep the ordinary credentials that accumulate around a life: logins, shared accounts, and the things I would rather not have to remember. Before 1Password, I used Dashlane and at least one other password manager whose name has completely left me. That is probably not the strongest possible endorsement of it.

1Password was different mostly because it stayed easy to use. It worked well enough, consistently enough, that I did not have to keep thinking about the password manager itself. Over the years, that reliability turned it into one of the pieces of software I trusted.

What surprised me later was that 1Password did not have to stop at the edge of personal password management. It could also take part in the infrastructure I was building at home.

## The problem my home server created

My home server is managed through Git and Argo CD. Git contains the Kubernetes resource names, namespaces, chart versions, and mappings between services and the credentials they expect. The credential values do not belong there.

As the server grew, so did the list of values to protect: database passwords, API tokens, storage credentials, private keys, and application secrets. I could have introduced a separate secret-management product. That would also have been another service to learn, protect, recover, and remember to maintain.

I had already done the slow part with 1Password. I trusted it, knew how to use it, and already paid for it through my family account. I created a dedicated home-server vault and an External Secrets Operator service account through that same account.

For new machine credentials, I prefer one credential per 1Password item. It keeps references and revocation easy to understand. Some existing home-server integrations group related fields, such as a database username and password, in one item. That is the history of the system; the one-credential convention is how I prefer to add new machine-facing credentials.

## What I use, and what I got wrong

[External Secrets Operator](https://external-secrets.io/?ref=neumachen.dev) connects 1Password to Kubernetes. Argo CD installs ESO and applies its custom resources. A `ClusterSecretStore` configures the 1Password SDK provider. Each [ExternalSecret](https://external-secrets.io/latest/api/externalsecret/?ref=neumachen.dev) selects one or more properties and tells ESO how to create a Kubernetes `Secret` for the consumer.

I chose ESO because it fit the GitOps boundary I already had. Git and Argo CD could declare which credential goes where while the values stayed in 1Password, and ESO could materialize the Kubernetes Secrets that existing consumers expect. The [1Password SDK provider](https://external-secrets.io/latest/provider/1password-sdk/?ref=neumachen.dev) also talks to 1Password without a separate 1Password Connect service. That let me reuse the secret authority I already trusted instead of adopting another complete secret-management platform.

I use 1Password through ESO on my home server. The concrete resources and mappings below come from the current GitOps repository, so they describe declared state rather than a fresh cluster-health check.

The service account is authorized to a vault, not merely to the field named by one `ExternalSecret`. Anyone who can create a suitable `ExternalSecret` and read its resulting Kubernetes Secret can request other data that service account may read. That fact exposed a flaw in my current arrangement.

The repository currently declares this bootstrap path:

### The boundary my repository currently declares

GitOpsHuman bootstrapTrust boundarySecret delivery

Current bootstrap boundary with a self-readable credentialGit and Argo CD install External Secrets Operator and configure the onepasswordSDK ClusterSecretStore. A manually seeded Kubernetes Secret authenticates the store. The service account can read the home-server vault containing both the Cloudflare credential and its own service-account token item, creating the bootstrap boundary flaw.GitOps and bootstrapService-readable vault and consumer pathGitArgo CDESO controller\+ CRDsESO service-account token iteminside the readable vaultonepassword-sa-tokenKubernetes SecretonepasswordSDKClusterSecretStoreService-readablehome-server vaultCloudflare API Credentialcredential fieldCloudflareExternalSecretcloudflare-api-token Secretapi-token keycert-manager ClusterIssuerDNS-01 manually seeded authenticatesuses containsselected by fieldmaterializesconsumed bycan read vaultalso containsretrieves 

Scroll sideways to follow the flow.

The service account can read the vault that contains its own bootstrap credential.

The token item is in the same custom vault the service account can read. An authorized `ExternalSecret` author could request ESO's provider token, materialize it in Kubernetes, and use it outside the cluster. Putting a replacement token beside it during rotation would reproduce the problem.

The safer target is:

### The safer boundary I am moving toward

GitOpsHuman bootstrapTrust boundarySecret delivery

Safer bootstrap boundary outside ESO's readable vaultA human operator reads the service-account token from a Private or bootstrap vault that ESO cannot access, then manually seeds the Kubernetes Secret. Git and Argo CD configure External Secrets Operator, which reads only the home-server vault and materializes selected item fields for Kubernetes consumers.Bootstrap and GitOpsRead-only provider access and Kubernetes deliveryPrivate or bootstrap vaultESO has no accessHuman operator\+ signed-in op CLIonepassword-sa-tokenKubernetes SecretGitArgo CDESO controller\+ CRDsonepasswordSDKClusterSecretStoreService-readablehome-server vaultSelected item fieldExternalSecretMaterializedKubernetes SecretController or workload human account reads manually seeds authenticatesuses read-only access 

Scroll sideways to follow the flow.

ESO's credential enters Kubernetes from outside Git and remains outside the vault ESO can read.

The bootstrap credential enters Kubernetes from outside Git and is not retrieved through ESO. This migration is still pending in my setup. Writing the guide is what made the misplaced boundary obvious; I have not changed the home-server repository, 1Password account, or cluster to make the story tidier.

## One real credential: Cloudflare to cert-manager

This is the path declared in my home-server repository. cert-manager uses a Cloudflare API token to complete DNS-01 challenges:

### From Argo CD and 1Password to the wildcard Certificate

GitOpsSecret delivery

Argo CD ownership and Cloudflare credential deliveryThe root app-of-apps creates the cert-manager, cert-manager-issuers, and traefik-config Applications. They own the chart and CRDs, the Cloudflare ExternalSecret plus both issuers, and the wildcard Certificate. The Cloudflare credential field passes through the onepassword ClusterSecretStore and ExternalSecret into the cloudflare-api-token Kubernetes Secret. Both issuers use that Secret, and the wildcard Certificate names the production issuer.Argo CD ownershipCredential deliveryRoot app-of-appscert-managerApplicationcert-manager-issuersApplicationtraefik-configApplicationcert-manager chart\+ CRDsCloudflare ExternalSecret\+ staging and production issuersWildcard CertificateCloudflare API Credentialcredential fieldonepasswordClusterSecretStoreCloudflareExternalSecretcloudflare-api-token Secretapi-token keyletsencrypt-staging\+ letsencrypt-prodWildcard Certificateuses letsencrypt-prod selected throughremoteRefmaterializesshared credentialproduction issuer 

Scroll sideways to follow the flow.

Purple shows Argo CD ownership. Green shows the credential handoff. Names that identify private infrastructure are sanitized in the article.

The private domain, email address, vault name, Git owner, 1Password item title, and wildcard Certificate identity in this article are sanitized.

The 1Password item is an **API Credential**. Its built-in `credential` field contains the Cloudflare token. ESO maps that field to the `api-token` key in a Secret named `cloudflare-api-token`, which the `ClusterIssuer` references. [cert-manager recommends an API token instead of the global API key](https://cert-manager.io/docs/configuration/acme/dns01/cloudflare/?ref=neumachen.dev) and documents `Zone - DNS - Edit` plus `Zone - Zone - Read`. Its current example applies those permissions to all zones. [Restricting the token to a particular zone](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/?ref=neumachen.dev) is a separate least-privilege choice that should be tested against actual issuance; the repository does not establish that my current token uses that narrower scope.

## Reproducing the design

The walkthrough assumes that Argo CD already exists. You also need `kubectl`, a human-authenticated 1Password CLI session, and an Argo CD AppProject that permits the repositories, cluster, and namespaces below.

Blocks labelled **repository excerpt** match the current repository except for stated sanitization. An **operator step** happens outside Git. A **pending procedure** has been checked without using the live cluster.

These values are environment-specific:

| Example value                                                                    | Replace or verify                                                    |
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| argocd                                                                           | Namespace containing Argo CD Application objects                     |
| spec.project: home                                                               | Existing AppProject with the necessary permissions                   |
| git@github.com:YOUR-ORG/home-server.git                                          | Repository URL                                                       |
| gitops/01-core/external-secretsgitops/01-core/cert-managergitops/01-core/traefik | Repository paths                                                     |
| 2.9.0                                                                            | ESO chart version                                                    |
| v1.21.1                                                                          | cert-manager chart version                                           |
| Infrastructure Secrets                                                           | Custom infrastructure vault                                          |
| Private                                                                          | Human operator's built-in vault                                      |
| external-secrets, cert-manager, edge                                             | Kubernetes namespaces; edge sanitizes the real Certificate namespace |
| cloudflare-dns-token                                                             | Sanitized 1Password item title                                       |
| wildcard-example-net, wildcard-example-net-tls                                   | Sanitized Certificate and TLS Secret names                           |
| example.net, admin@example.net                                                   | Sanitized DNS zone and ACME email                                    |

As of October 2026, my repository pins ESO chart version `2.9.0` and uses `external-secrets.io/v1`. Check the current [ESO installation guide](https://external-secrets.io/latest/introduction/getting-started/?ref=neumachen.dev), [1Password SDK provider documentation](https://external-secrets.io/latest/provider/1password-sdk/?ref=neumachen.dev), and upgrade notes before copying those versions.

I created my service account through the same 1Password family account I use personally. As of October 2026, 1Password documents service accounts and account-specific limits for Families accounts. Verify availability and your permission to create one against the current [service-account requirements](https://www.1password.dev/service-accounts/get-started?ref=neumachen.dev) and [rate-limit documentation](https://www.1password.dev/service-accounts/rate-limits/?ref=neumachen.dev).

### Set the 1Password access boundaries

Create a custom infrastructure vault for data ESO may fetch. Create a service account in 1Password's Developer area with read access to only that vault. Do not grant write access or permission to create vaults for this use.

Save its token as an **API Credential** item named `home-server-eso-bootstrap`, with the token in the built-in `credential` field. Put that item in the human operator's built-in Private vault, or in a separate bootstrap vault that is never granted to the service account. 1Password says built-in Personal, Private, Employee, and default Shared vaults cannot be granted to service accounts, which makes Private a useful boundary. The creation wizard shows the token once, so save it immediately.

Create the Cloudflare **API Credential** item in the infrastructure vault. In this article its sanitized title is `cloudflare-dns-token` and the token is in `credential`.

Vault grants and permissions are immutable after a service account is created. If they need to change, create a new service account. If they stay the same, 1Password supports rotating the existing account's token; that distinction matters later.

### Install ESO through Argo CD

**Repository excerpt:** this Application installs ESO and its CRDs. Only comments have been omitted.

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: external-secrets
  namespace: argocd
  annotations:
    argocd.argoproj.io/sync-wave: "0"
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: home
  source:
    repoURL: https://charts.external-secrets.io
    chart: external-secrets
    targetRevision: 2.9.0
    helm:
      valuesObject:
        installCRDs: true
  destination:
    server: https://kubernetes.default.svc
    namespace: external-secrets
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
      - ServerSideApply=true

```

`CreateNamespace=true` declares the `external-secrets` namespace. In my app-of-apps layout, this file lives at `gitops/01-core/external-secrets/application.yaml`.

```sh
kubectl get namespace external-secrets
kubectl -n external-secrets get deployment,pods
kubectl get crd clustersecretstores.external-secrets.io
kubectl get crd externalsecrets.external-secrets.io

```

### Seed ESO's bootstrap credential

**Operator step, pending in my setup:** set `OP_ACCOUNT` to the human account shorthand, sign-in address, account ID, or user ID shown by `op account list`. The explicit account selector matters when more than one 1Password account is configured. The environment cleanup prevents an inherited service-account or Connect credential from silently changing which identity performs the read.

`Private` is a vault title, not a cross-account identity. If that title is ambiguous or has been localized, get the intended vault ID with `op vault list --account "$OP_ACCOUNT"` and use that ID in the first segment of the `op://` reference. 1Password secret references accept names or unique IDs.

Run the command only after the ESO namespace exists. It does not overwrite an existing Secret.

```sh
OP_ACCOUNT='your-human-1password-account' /bin/bash <<'BASH'
set -euo pipefail
set +x

unset OP_SERVICE_ACCOUNT_TOKEN OP_CONNECT_HOST OP_CONNECT_TOKEN
eso_token=''
trap 'unset eso_token' EXIT

if [[ -z "${OP_ACCOUNT:-}" ]]; then
  printf '%s\n' 'error: OP_ACCOUNT must select the human 1Password account' >&2
  exit 1
fi

eso_token="$(op read --account "$OP_ACCOUNT" \
  'op://Private/home-server-eso-bootstrap/credential')"
if [[ -z "$eso_token" ]]; then
  printf '%s\n' 'error: 1Password returned an empty service-account token' >&2
  exit 1
fi

printf '%s' "$eso_token" |
  kubectl create secret generic onepassword-sa-token \
    --namespace external-secrets \
    --from-file=token=/dev/stdin

unset eso_token
trap - EXIT
BASH

```

Check only metadata and key names:

```sh
kubectl -n external-secrets get secret onepassword-sa-token \
  -o go-template='{{.metadata.name}}{{"\n"}}{{range $key, $_ := .data}}{{println $key}}{{end}}'

```

The output should contain `onepassword-sa-token` and `token`, never the value.

### Declare the store in its own Application

**Repository excerpt, repository URL sanitized:** the second Application owns the raw store resource.

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: external-secrets-store
  namespace: argocd
  annotations:
    argocd.argoproj.io/sync-wave: "1"
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: home
  source:
    repoURL: git@github.com:YOUR-ORG/home-server.git
    path: gitops/01-core/external-secrets
    targetRevision: main
    directory:
      recurse: false
      exclude: '{application.yaml,application-*.yaml}'
  destination:
    server: https://kubernetes.default.svc
    namespace: external-secrets
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

```

**Repository excerpt, vault name sanitized:** the store uses the manually seeded Secret.

```yaml
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: onepassword
spec:
  provider:
    onepasswordSDK:
      vault: "Infrastructure Secrets"
      cache:
        ttl: 6h
        maxSize: 128
      auth:
        serviceAccountSecretRef:
          name: onepassword-sa-token
          namespace: external-secrets
          key: token

```

My root Application is configured to create the two External Secrets child Applications, `external-secrets` and `external-secrets-store`, automatically. Their sync waves order the child Application resources; they do not wait for a human to seed the bootstrap Secret. On a new cluster the store may therefore be temporarily `NotReady` until the Secret appears, after which ESO can reconcile it. The bootstrap remains operator-controlled.

The six-hour cache is a current request-budget mitigation, not a general recommendation. ESO calls the cache experimental and warns that long TTLs can return stale data. Its longer-term effectiveness in my environment is not yet established.

```sh
kubectl wait --for=condition=Ready \
  clustersecretstore/onepassword \
  --timeout=120s
kubectl get clustersecretstore onepassword

```

### Reconcile cert-manager and its raw resources

My root app-of-apps is configured to discover the child Application manifests under `gitops/01-core`. The diagram above shows the relevant ownership chain: the chart Application owns cert-manager and its CRDs, the issuer Application owns the Cloudflare `ExternalSecret` and both issuers, and `traefik-config` owns the wildcard `Certificate`.

**Repository excerpts, Git owner sanitized:** the chart Application installs cert-manager and creates its namespace. The issuer Application reconciles the raw YAML in the same directory while excluding the flattened Application manifests.

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cert-manager
  namespace: argocd
  annotations:
    argocd.argoproj.io/sync-wave: "0"
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: home
  source:
    repoURL: https://charts.jetstack.io
    chart: cert-manager
    targetRevision: v1.21.1
    helm:
      valuesObject:
        crds:
          enabled: true
  destination:
    server: https://kubernetes.default.svc
    namespace: cert-manager
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cert-manager-issuers
  namespace: argocd
  annotations:
    argocd.argoproj.io/sync-wave: "1"
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: home
  source:
    repoURL: git@github.com:YOUR-ORG/home-server.git
    path: gitops/01-core/cert-manager
    targetRevision: main
    directory:
      recurse: false
      exclude: '{application.yaml,application-*.yaml}'
  destination:
    server: https://kubernetes.default.svc
    namespace: cert-manager
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

```

As of October 2026, the repository pins cert-manager chart version `v1.21.1`. Check the current [release and upgrade documentation](https://cert-manager.io/docs/releases/?ref=neumachen.dev) before copying it. If your cert-manager Application does not create the namespace, declare `cert-manager` before applying the namespaced resources that follow.

A `ClusterIssuer` is cluster-scoped, but cert-manager reads its referenced Secrets from the configured [cluster resource namespace](https://cert-manager.io/docs/configuration/?ref=neumachen.dev), which defaults to `cert-manager`. The repository therefore places the `ExternalSecret` and its generated Secret there.

### Deliver the Cloudflare token

**Repository excerpt, item title sanitized:** this `ExternalSecret` is owned by `cert-manager-issuers`.

```yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: cloudflare-api-token
  namespace: cert-manager
  annotations:
    argocd.argoproj.io/sync-wave: "1"
spec:
  secretStoreRef:
    name: onepassword
    kind: ClusterSecretStore
  refreshInterval: 1h
  target:
    name: cloudflare-api-token
    creationPolicy: Owner
  data:
    - secretKey: api-token
      remoteRef:
        key: cloudflare-dns-token/credential

```

**Repository excerpt, email and zone sanitized:** staging and production use different ACME endpoints and account-key Secrets, but both read the same Cloudflare Secret and key.

```yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
  annotations:
    argocd.argoproj.io/sync-wave: "2"
spec:
  acme:
    email: admin@example.net
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-staging-account-key
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token
        selector:
          dnsZones:
            - example.net
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
  annotations:
    argocd.argoproj.io/sync-wave: "2"
spec:
  acme:
    email: admin@example.net
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token
        selector:
          dnsZones:
            - example.net

```

### Reconcile the wildcard Certificate

The Certificate lives with the Traefik configuration in my GitOps layout. **Repository excerpts, Git owner and private identities sanitized:** the namespace, Certificate name, TLS Secret name, and domain are changed while the repository structure is preserved.

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: traefik-config
  namespace: argocd
  annotations:
    argocd.argoproj.io/sync-wave: "2"
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: home
  source:
    repoURL: git@github.com:YOUR-ORG/home-server.git
    path: gitops/01-core/traefik
    targetRevision: main
    directory:
      recurse: false
      exclude: '{application.yaml,application-*.yaml}'
  destination:
    server: https://kubernetes.default.svc
    namespace: edge
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-example-net
  namespace: edge
  annotations:
    argocd.argoproj.io/sync-wave: "2"
spec:
  secretName: wildcard-example-net-tls
  dnsNames:
    - example.net
    - "*.example.net"
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
    group: cert-manager.io

```

It is the wildcard Certificate my repository declares, with private identities changed. The production issuer follows the shared `cloudflare-api-token` reference back through the `ExternalSecret` to `cloudflare-dns-token/credential` in 1Password.

## Verify the path without exposing the value

Check the store, synchronization status, and generated Secret shape:

```sh
kubectl get clustersecretstore onepassword
kubectl -n cert-manager wait \
  --for=condition=Ready externalsecret/cloudflare-api-token \
  --timeout=120s
kubectl -n cert-manager get externalsecret cloudflare-api-token
kubectl -n cert-manager get secret cloudflare-api-token \
  -o go-template='type={{.type}}{{"\n"}}name={{.metadata.name}}{{"\n"}}keys={{range $key, $_ := .data}}{{printf "%s " $key}}{{end}}{{"\n"}}'

```

The expected data key is `api-token`. Do not decode it for a routine check.

Then check both issuers and the sanitized Certificate shown above:

```sh
kubectl get clusterissuer letsencrypt-staging letsencrypt-prod

CERT_NAMESPACE='edge'
CERT_NAME='wildcard-example-net'
kubectl -n "$CERT_NAMESPACE" get certificate "$CERT_NAME"
kubectl -n "$CERT_NAMESPACE" describe certificate "$CERT_NAME"
kubectl -n "$CERT_NAMESPACE" get certificaterequest,order,challenge

```

A recent successful DNS-01 issuance or renewal exercises the Cloudflare credential. A Certificate that has merely remained `Ready` is weaker evidence because it may be using previously materialized state.

## Rotating the bootstrap credential without fooling myself

1Password's [current rotation procedure](https://www.1password.dev/service-accounts/manage-service-accounts/?ref=neumachen.dev) generates a new token for the same service account and permissions. In the web app, open **Developer**, select the service account, choose **Rotate Token**, and set an expiration for the current token long enough to update and test the replacement. Save the new token into Private or the excluded bootstrap vault. There is no need to create a new service account unless the vault access or permissions must change.

**Pending procedure:** after the new token has been saved, replace the Kubernetes value with the same defensive handling. Retrieval must succeed before `kubectl apply` can touch the working Secret.

```sh
OP_ACCOUNT='your-human-1password-account' /bin/bash <<'BASH'
set -euo pipefail
set +x

unset OP_SERVICE_ACCOUNT_TOKEN OP_CONNECT_HOST OP_CONNECT_TOKEN
replacement_token=''
trap 'unset replacement_token' EXIT

if [[ -z "${OP_ACCOUNT:-}" ]]; then
  printf '%s\n' 'error: OP_ACCOUNT must select the human 1Password account' >&2
  exit 1
fi

replacement_token="$(op read --account "$OP_ACCOUNT" \
  'op://Private/home-server-eso-bootstrap/credential')"
if [[ -z "$replacement_token" ]]; then
  printf '%s\n' 'error: 1Password returned an empty replacement token' >&2
  exit 1
fi

printf '%s' "$replacement_token" |
  kubectl create secret generic onepassword-sa-token \
    --namespace external-secrets \
    --from-file=token=/dev/stdin \
    --dry-run=client -o yaml |
  kubectl apply -f -

unset replacement_token
trap - EXIT
BASH

```

Here is the part I cannot yet turn into a tested production runbook. With a six-hour provider cache, an `ExternalSecret` can remain `Ready`, its Secret can exist, and its consumer can work without proving that ESO has authenticated with the replacement token. Forcing an ordinary refresh is not enough if the requested value is still cached. I have not established an authoritative, tested fresh-auth check for this provider setup.

That means I would not choose a short expiration window, or revoke the old token, based only on those green statuses. The overlap must be long enough for a controlled test that demonstrates an uncached provider read using the replacement. Until I have exercised and documented that test, this rotation section remains a proposed procedure rather than a runbook.

For my pending migration, the obsolete token item should leave the service-readable infrastructure vault only after that fresh-auth test and the old token's safe expiry or revocation. Until then, the flawed boundary still exists.

Application credentials have another clock. My Cloudflare `ExternalSecret` is configured to reconcile hourly, the provider cache may serve its old value for six hours, and the final consumer may not reload a changed Secret automatically. Rotation is complete only when the consumer demonstrably uses the new credential.

## Why I still use it

This arrangement does not make Kubernetes Secrets harmless. They still need appropriate RBAC, encryption-at-rest decisions, protected backups, and care around who can create `ExternalSecret` resources. A cluster-wide store is a large trust boundary, and ESO's bootstrap credential needs its own recovery and rotation procedure.

Those limitations did not undo the original choice. 1Password remains where I know how to inspect, replace, and recover a credential, while Git records the non-secret wiring that delivers it.

Infrastructure has a way of collecting products that are individually sensible and exhausting in combination. Reusing 1Password kept the work of secret management in a place I already knew.