From 1Password to Kubernetes with External Secrets Operator

My applications kept running when secret refreshes failed. Following a credential from 1Password into Kubernetes explains why, and what still needs checking.

When my home cluster hit a 1Password rate limit, the applications kept running. External Secrets Operator could no longer refresh their credentials, but the Kubernetes Secrets it had already created were still there. The applications carried on using them.

That was useful. It also meant that checking whether the applications worked would have missed the failure.

I use 1Password to hold the credentials that External Secrets Operator, or ESO, delivers to my cluster. Argo CD manages the configuration from Git. The applications receive ordinary Kubernetes Secrets. The arrangement keeps secret values out of the deployment repository, while leaving a few questions that the YAML alone cannot answer: who gets access, how the first credential arrives, and how long an old value can remain in use.

Where each piece belongs

ESO is a Kubernetes controller. It reads an external secret provider and writes the values into Kubernetes Secrets. An application can then consume them as environment variables or mounted files without knowing anything about 1Password.

My setup uses ESO's onepasswordSDK provider with a 1Password service account. That integration talks to 1Password without a Connect server. This article follows that particular setup. ESO's provider documentation describes its configuration and capabilities.

Git holds two kinds of instructions for ESO. A ClusterSecretStore identifies the provider, the vault, and the credential ESO should use. An ExternalSecret names the fields to retrieve and the keys to create in the target Secret. Argo CD applies those instructions; ESO handles the values.

The values travel along this path:

1Password item field -> ESO -> Kubernetes Secret -> application

The benefit is practical: a Git change can show that an application now needs a particular credential without including the credential itself. Those references can still reveal something about the system, so keeping values out of Git does not automatically make the repository suitable for public access.

ESO also leaves a copy of the secret in Kubernetes. Access controls, encryption at rest, and protection of backups still belong in the cluster's configuration. Kubernetes Secrets are not encrypted in etcd by default. Kubernetes documents the storage and access implications.

The first credential still needs a way in

ESO needs a credential before it can fetch credentials. In this setup, that is a 1Password service-account token, placed in a Kubernetes Secret outside the normal GitOps reconciliation. The service account has read-only access to the vault used for cluster credentials.

The store manifest contains the name, namespace, and key of that bootstrap Secret. The token itself stays out of Git, shell history, logs, and the article you are reading.

There is another bootstrap dependency in my repository: the Argo CD installation instructions also create a Secret containing the deploy key for the private Git repository. ESO's service-account token is one part of getting the cluster started. Calling it the only manually seeded Secret would leave out how Argo gets through the front door.

A fresh-cluster recovery procedure has to account for both. So does the process for replacing either credential when it expires or needs to be revoked.

Following one token

Here is a small example using made-up names. It assumes ESO and its CRDs are installed, the namespaces exist, and a service-account token has already been seeded in external-secrets. These examples use the external-secrets.io/v1 API; check the installed provider and CRDs before applying them.

The vault is called example-vault. It contains an item called demo-app, with a field called token. The service account can read that vault.

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: onepassword-demo
spec:
  conditions:
    - namespaces:
        - demo
  provider:
    onepasswordSDK:
      vault: example-vault
      auth:
        serviceAccountSecretRef:
          name: op-service-account
          namespace: external-secrets
          key: token
      cache:
        ttl: 6h
        maxSize: 128

The namespace condition limits use of this store to demo. The six-hour cache matches the mitigation discussed below; its effect on rotation matters as much as its effect on request counts. The ClusterSecretStore reference explains how namespace conditions work.

The ExternalSecret maps the item field to a key the application understands:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: demo-app-credentials
  namespace: demo
spec:
  refreshPolicy: Periodic
  refreshInterval: 1h
  secretStoreRef:
    name: onepassword-demo
    kind: ClusterSecretStore
  target:
    name: demo-app-credentials
    creationPolicy: Owner
    deletionPolicy: Retain
  data:
    - secretKey: API_TOKEN
      remoteRef:
        key: demo-app/token

The store supplies the vault; demo-app/token supplies the item and field. The provider also supports <item>/[section/]<field> for fields inside a section. Names and IDs are both available in 1Password secret references. Simple, consistent names make a manifest easier to read; IDs can be useful when a name contains unsupported characters.

ESO creates demo-app-credentials in the demo namespace with an API_TOKEN key. A container in that namespace can read it with this fragment in its spec:

env:
  - name: API_TOKEN
    valueFrom:
      secretKeyRef:
        name: demo-app-credentials
        key: API_TOKEN

That is the whole mapping: one field in 1Password, one key in Kubernetes, and one environment variable in the application. The ExternalSecret API reference covers the resource's refresh and target settings.

More complicated applications may need a connection string assembled from several fields. ESO templates can do that. With mergePolicy: Replace, only the template's output keys are included in the resulting Secret, so a helper field need not appear as an additional key. Its value may still be present in the assembled connection string; omitting the helper key does not hide that value from the application. ESO's templating documentation explains the distinction.

Who is allowed to ask for a secret?

A shared store makes it convenient to deliver credentials across the cluster. It also makes the right to create an ExternalSecret a meaningful permission. Someone who can change the requested reference may be able to have ESO copy a different item from the same vault into their namespace.

Restricting who can read the resulting Kubernetes Secret is only part of that boundary. Access to the ESO resources matters too, including changes that arrive through Git and Argo CD. The namespace restriction in the example limits where the store can be used; it does not restrict each application to one item within the vault. ESO's security guidance covers these controls.

My current store manifest is shared across the cluster and does not specify namespace conditions. The example above is narrower. A home cluster managed by its owner and a platform shared by independent teams make different trust assumptions. Before adopting this arrangement for a team, decide who can change the references and what the provider credential lets those references reach.

An hourly refresh can use an older value

On September 12, 2026, the setup had 14 ExternalSecrets containing 36 direct remote references. Each resource refreshed hourly, with provider caching omitted. That gives a simple planning calculation:

36 references x 24 refreshes = 864 logical resolves per day

The account's shared daily allowance was 1,000 requests, and the incident check found it exhausted. The service-account token still had room in its hourly allowance. Creating another token would not have restored the shared budget. 1Password documents hourly token limits and daily account limits; the allowances depend on the account type.

The calculation explains why the configuration deserved attention. It does not prove that each logical resolve consumed exactly one quota-counted request, or that ESO accounted for all of the account's usage. Validation, retries, and other automation also need room in the budget.

The mitigation added a six-hour provider cache while retaining hourly reconciliation. That change is recorded in the configuration. A completed 24-hour measurement of its effect is still absent from the incident record, so there is no measured before-and-after request count to report here.

Caching also changes the meaning of a successful refresh. ESO may reconcile hourly and receive a value that is still in the cache. The provider's documentation marks caching experimental and warns about stale values with long TTLs. The six-hour setting needs to be considered alongside how quickly the applications need a replacement credential.

There is another delay after ESO updates the Kubernetes Secret. A container that receives a value through an environment variable keeps that value until it restarts. Mounted Secret files can update, but the application has to read the changed file; a subPath mount does not receive automatic Secret updates. Kubernetes documents the behavior for environment variables and mounted files.

For an API token, rotation therefore includes creating the replacement at the issuing service, updating the value in 1Password, allowing ESO to deliver it, and getting the application to use it. Revoking the old token has to fit that sequence. Changing a field in 1Password alone does not rotate the credential at the service that issued it.

Neither the cache TTL nor the reconciliation interval establishes how long that complete sequence takes.

When the provider is unavailable

During the rate-limit incident, the existing Kubernetes Secrets remained available. That gave the applications some independence from the provider, as long as their existing credentials were still valid.

A new workload with no Secret to consume has no such buffer. Neither does a fresh cluster waiting to retrieve its first set of credentials. A retained copy also cannot make a revoked credential work again.

Deletion is a separate case. In the example, deletionPolicy: Retain covers the provider-side deletion behavior. It does not protect the Secret from deletion of its owning ExternalSecret: creationPolicy: Owner establishes an owner reference, and Kubernetes can garbage-collect the Secret when that owner is removed. This matters when pruning resources through GitOps. ESO's lifecycle documentation describes the separate policies.

I still need to change a non-production value and follow it into a running application. That test would tell me how long rotation actually takes in this setup. For now, that timing is an open question.

I also want better observability around the services this setup depends on. At the very least, I want to watch for rate limits on services like 1Password and get an alert when secret refreshes start failing. Where quota usage is available, I'd like to see the remaining allowance before requests start getting rejected.

For ESO, that means watching the store's readiness, failed refreshes, provider errors, and the time since the last successful reconciliation, without printing secret values. A successful reconciliation can still use a cached value, so the cache settings need to be part of that picture. I want the warning while the old credentials are still keeping things running.