# Psono Kubernetes Operator

The Psono Kubernetes Operator synchronizes selected values from Psono into native Kubernetes Secrets. Applications can consume the generated Secrets through env, envFrom, or Secret-backed volumes.

The operator uses a restricted Psono API key and decrypts each entry locally in the cluster. It has read-only access to Psono and cannot create, modify, or delete Psono entries.

# Concepts

Four objects participate in a synchronization:

Object Purpose
Psono entry Holds the source values. This can be an Environment Variables entry or another typed entry.
Restricted Psono API key Grants read access only to the Psono entries required by the workload.
Authentication Kubernetes Secret Holds the Psono server URL, API key ID, and API secret key.
PsonoSecret Selects source values and describes the target Kubernetes Secret.

The generated target is an ordinary Kubernetes Secret. It is not the same as the authentication Secret and it does not contain the Psono API credentials.

For each synchronization, the operator:

  1. Reads the authentication Secret from the PsonoSecret namespace.
  2. Calls Psono's sessionless /api-key-access/secret/ endpoint.
  3. Receives the encrypted entry and its encrypted per-entry key.
  4. Decrypts both layers locally using XSalsa20-Poly1305.
  5. Extracts only the values listed in the PsonoSecret.
  6. Creates or updates the target Kubernetes Secret.
  7. Repeats the operation at the configured refresh interval.

Failures while reading, decrypting, extracting, or updating values leave the target at its last successfully synchronized state. Cleanup or status errors can be reported after the current target was already updated. The operator reports every failure through the PsonoSecret status.

# Requirements

Before installing the operator, ensure that you have:

  • Kubernetes 1.32 or newer.
  • Helm 3 with OCI registry support.
  • Network access from the operator to the Kubernetes API and your Psono server.
  • A Psono account that can access the required entries.
  • A dedicated, restricted, read-only Psono API key.
  • The required Psono entries assigned to that API key.
  • "Allow insecure usage" disabled for the API key.

The operator needs the API key ID and API secret key. It does not use the API private/signing key.

# Prepare Psono

# Create the source entries

Create or select the entries containing the values that Kubernetes should receive. See Creating secrets for the general Psono workflow.

The simplest source for application configuration is a Psono Environment Variables entry. One such entry can contain multiple key/value pairs, for example:

Key Value
DB_HOST postgres.production.svc
DB_USER application
DB_PASSWORD a-secret-password

The operator does not copy the complete entry automatically. Every value must be selected explicitly in the PsonoSecret. This keeps the exported values limited to an explicit allowlist.

Other Psono entry types are supported through field selectors. The selected field must contain a string.

# Create a restricted API key

Follow the API key creation guide and apply these operator-specific restrictions:

  1. Create a dedicated API key for one application, namespace, or equivalent trust boundary.
  2. Grant read access only.
  3. Assign only the Psono entries required by that workload.
  4. Leave "Allow insecure usage" disabled.
  5. Record the API key ID and API secret key.
  6. Record the SECRET_ID shown for every assigned entry.

The operator performs sessionless access with local decryption. Do not grant write access merely because another integration using the same entry needs it. Use separate API keys for readers and writers.

# Install the operator

Install a versioned release from the OCI Helm registry. The following example installs version 1.0.0:

helm upgrade --install psono-kubernetes-operator \
  oci://registry-1.docker.io/psono/psono-kubernetes-operator \
  --version 1.0.0 \
  --namespace psono-kubernetes-operator-system \
  --create-namespace \
  --wait

Use the same version for the Helm chart and the desired operator release. Replace 1.0.0 with another published version when required. Release images support Linux on AMD64 and ARM64.

WARNING

The default installation watches PsonoSecret resources across the cluster and has permission to read, create, update, and delete Kubernetes Secrets. Restrict who can deploy the operator and who can create PsonoSecret resources.

Common Helm settings include:

Setting Purpose
replicaCount Number of operator replicas. Leader election is enabled by default.
image.repository Override the operator image repository.
image.tag Override the image tag. The chart app version is used by default.
image.pullPolicy Kubernetes image pull policy.
imagePullSecrets Credentials for a private image registry.
serviceAccount.create Create a ServiceAccount for the operator.
serviceAccount.name Use a specific ServiceAccount name.
leaderElection Enable leader election between replicas.
resources Configure CPU and memory requests and limits.
nodeSelector, tolerations, affinity Control operator scheduling.
podSecurityContext, securityContext Override the hardened default security contexts.

Verify the installation without displaying any secret values:

helm status psono-kubernetes-operator \
  --namespace psono-kubernetes-operator-system

kubectl get crd psonosecrets.secrets.psono.com

kubectl get pods \
  --namespace psono-kubernetes-operator-system

# Create the authentication Secret

Create an authentication Secret in every namespace where a PsonoSecret will be used. The authentication Secret and the PsonoSecret that references it must be in the same namespace.

Create the workload namespace first if it does not already exist:

kubectl create namespace production
apiVersion: v1
kind: Secret
metadata:
  name: psono-operator-credentials
  namespace: production
type: Opaque
stringData:
  serverUrl: https://psono.example.com/server
  apiKeyId: REPLACE_WITH_RESTRICTED_API_KEY_ID
  apiSecretKey: REPLACE_WITH_64_CHARACTER_HEX_API_SECRET_KEY

serverUrl is the externally reachable Psono server URL, including a path such as /server when applicable. apiSecretKey is a 32-byte key represented by 64 hexadecimal characters.

WARNING

Do not commit real API credentials to source control. Create this bootstrap Secret through your organization's approved secret-delivery mechanism and restrict access to it with Kubernetes RBAC.

The authentication Secret name and spec.target.name must be different. The operator does not enforce this restriction. Reusing the authentication Secret as a target could overwrite or expose the Psono API credentials.

The key names can be changed when required:

authentication:
  secretRef:
    name: psono-operator-credentials
    serverUrlKey: psono-url
    apiKeyIdKey: psono-api-key-id
    apiSecretKeyKey: psono-api-secret-key

# Trust a private certificate authority

For a Psono server using a certificate issued by a private CA, add the PEM CA bundle to the authentication Secret:

apiVersion: v1
kind: Secret
metadata:
  name: psono-operator-credentials
  namespace: production
type: Opaque
stringData:
  serverUrl: https://psono.example.com/server
  apiKeyId: REPLACE_WITH_RESTRICTED_API_KEY_ID
  apiSecretKey: REPLACE_WITH_64_CHARACTER_HEX_API_SECRET_KEY
  ca.crt: |
    -----BEGIN CERTIFICATE-----
    ...
    -----END CERTIFICATE-----

Reference the bundle from the PsonoSecret:

authentication:
  secretRef:
    name: psono-operator-credentials
    caBundleKey: ca.crt

The CA bundle augments the system trust roots. It must contain valid PEM certificates. TLS verification cannot be disabled.

# Synchronize an Environment Variables entry

Suppose one Psono Environment Variables entry contains DB_HOST, DB_USER, and DB_PASSWORD. Map each required value explicitly while reusing the same Psono secretId:

apiVersion: secrets.psono.com/v1alpha1
kind: PsonoSecret
metadata:
  name: database-credentials
  namespace: production
spec:
  refreshInterval: 5m
  authentication:
    secretRef:
      name: psono-operator-credentials
  target:
    name: database-credentials
    creationPolicy: Owner
    type: Opaque
  data:
    - secretKey: DB_HOST
      remoteRef:
        secretId: 25070c66-8950-4264-9b39-11e6d83312e3
        environmentVariable: DB_HOST
    - secretKey: DB_USER
      remoteRef:
        secretId: 25070c66-8950-4264-9b39-11e6d83312e3
        environmentVariable: DB_USER
    - secretKey: DB_PASSWORD
      remoteRef:
        secretId: 25070c66-8950-4264-9b39-11e6d83312e3
        environmentVariable: DB_PASSWORD

The operator downloads and decrypts that Psono entry once during each reconciliation, then extracts all three requested values. The resulting Kubernetes Secret contains:

apiVersion: v1
kind: Secret
metadata:
  name: database-credentials
  namespace: production
type: Opaque
data:
  DB_HOST: <base64-encoded value>
  DB_USER: <base64-encoded value>
  DB_PASSWORD: <base64-encoded value>

environmentVariable performs a case-sensitive key match after trimming leading and trailing whitespace from the selector. If an entry contains the same key more than once, the first matching value is selected.

The output key can differ from the Psono key:

- secretKey: DATABASE_PASSWORD
  remoteRef:
    secretId: 25070c66-8950-4264-9b39-11e6d83312e3
    environmentVariable: DB_PASSWORD

# Synchronize fields from other entry types

Use field instead of environmentVariable to select a string from another Psono entry type:

data:
  - secretKey: API_PASSWORD
    remoteRef:
      secretId: 57c616ac-7c46-4a52-8936-670a607f2755
      field: website_password_password

Nested JSON objects can be traversed with a dot-separated path:

field: database.credentials.password

Array indexing is not supported. The selected value must be a string. Exactly one of field or environmentVariable must be specified for each mapping.

Mappings from different Psono entries can be combined in one target Secret. Each secretKey in a PsonoSecret must be unique.

# Target creation policies

spec.target.creationPolicy controls ownership and updates of the target Kubernetes Secret:

Policy Existing unrelated keys Mapping removed PsonoSecret deleted
Owner Replaced Key is removed on the next successful sync Target Secret is garbage-collected
Merge Preserved Previously managed key is removed on the next successful sync Target Secret and synchronized values remain

Owner is the default and is appropriate when the operator exclusively manages the target Secret. It rejects a target controlled by another Kubernetes controller.

Use Merge when another process also writes unrelated keys to the target. The operator tracks which keys each PsonoSecret manages and rejects configurations where multiple PsonoSecret resources try to manage the same target key.

Changing a target name or creation policy affects lifecycle and cleanup behavior. Test such changes before applying them in production.

When a Merge target is renamed, the old Secret and its synchronized values remain. The operator removes its ownership annotation but does not remove the old values. Remove obsolete keys or the old Secret manually after verifying the new target.

spec.target.type defaults to Opaque. When using a special Kubernetes Secret type, use only a type whose requirements can be satisfied entirely through data. For example, kubernetes.io/tls can be used when both tls.crt and tls.key are mapped. Types requiring annotations or another controller are not supported.

# Consume the generated Secret

Load all keys as container environment variables:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: application
  namespace: production
spec:
  replicas: 1
  selector:
    matchLabels:
      app: application
  template:
    metadata:
      labels:
        app: application
    spec:
      containers:
        - name: application
          image: example/application:1.0.0
          envFrom:
            - secretRef:
                name: database-credentials

Or select one key:

env:
  - name: DATABASE_PASSWORD
    valueFrom:
      secretKeyRef:
        name: database-credentials
        key: DB_PASSWORD

To mount values as files:

volumes:
  - name: database-credentials
    secret:
      secretName: database-credentials

containers:
  - name: application
    volumeMounts:
      - name: database-credentials
        mountPath: /var/run/secrets/application
        readOnly: true

Kubernetes eventually updates Secret-backed volumes after synchronization. Environment variables are fixed when a container starts, so workloads using env or envFrom need a rollout or a separate restart controller to receive changed values.

# Verify synchronization

Inspect resource status without printing secret contents:

kubectl get psonosecrets \
  --namespace production

kubectl describe psonosecret database-credentials \
  --namespace production

kubectl get secret database-credentials \
  --namespace production

The PsonoSecret list displays Ready, Target, Last Sync, and Age. A successful synchronization reports a Ready=True condition with reason SecretSynchronized.

The default refresh interval is five minutes. A custom refreshInterval must be a valid Go duration such as 30s, 5m, or 1h, and cannot be shorter than 30 seconds.

# Troubleshooting

Start with the resource condition:

kubectl describe psonosecret database-credentials \
  --namespace production

Then inspect the operator logs:

kubectl logs \
  --namespace psono-kubernetes-operator-system \
  --selector app.kubernetes.io/instance=psono-kubernetes-operator \
  --all-containers=true

Common status reasons are:

Reason Check
InvalidRefreshInterval Confirm that the value is a valid Go duration and at least 30 seconds.
InvalidConfiguration Check required fields, duplicate output keys, and that every mapping has exactly one selector.
AuthenticationSecretError Confirm that the authentication Secret is in the same namespace and contains all referenced keys.
ClientConfigurationError Check the server URL, 64-character API secret key, and optional CA bundle.
RemoteReadFailed Check network access, the Psono server response, API-key permissions, and the assigned SECRET_ID.
ValueExtractionFailed Check case-sensitive Environment Variables keys, field paths, and that the selected value is a string.
TargetUpdateFailed Check Kubernetes RBAC, target ownership, managed-key conflicts, and the requested Secret type.
PreviousTargetCleanupFailed For an old Owner target, check deletion and ownership. For an old Merge target, check whether its ownership annotation can be updated.

Do not include API credentials, decrypted Psono values, or decoded Kubernetes Secret values in logs, screenshots, or support requests.

# Security considerations

  • Use a separate restricted API key for each namespace, application, or trust boundary.
  • Grant each API key access only to the required Psono entries.
  • Restrict access to the authentication Secret, generated Secrets, and PsonoSecret resources.
  • Treat permission to create a PsonoSecret as secret-read permission. A user who can create one can export any entry available to credentials in that namespace.
  • Enable Kubernetes Secret encryption at rest. Kubernetes data values are base64-encoded, not encrypted by the operator.
  • Apply NetworkPolicies that allow required DNS resolution and access only to the Kubernetes API and required Psono endpoint.
  • Use HTTPS in production. Plain HTTP should be limited to isolated local development.
  • Never place secret values in PsonoSecret fields, labels, annotations, logs, or status.
  • Rotate the restricted API key and update the authentication Secret according to your organization's credential policy.
  • Pin operator releases instead of using mutable image tags.

# Upgrade or remove the operator

Helm does not upgrade CRDs stored in a chart's crds directory. Apply the CRD from the new chart before upgrading the controller:

export OPERATOR_VERSION=NEW_VERSION

helm show crds \
  oci://registry-1.docker.io/psono/psono-kubernetes-operator \
  --version "$OPERATOR_VERSION" | kubectl apply -f -

helm upgrade psono-kubernetes-operator \
  oci://registry-1.docker.io/psono/psono-kubernetes-operator \
  --version "$OPERATOR_VERSION" \
  --namespace psono-kubernetes-operator-system \
  --wait

Review release notes and CRD changes before upgrading.

Every reconciled PsonoSecret has a finalizer that must be processed by the operator. Handle and delete these resources while the operator is still running:

  1. List all resources with kubectl get psonosecrets --all-namespaces.
  2. Decide whether each target should be deleted or retained.
  3. To retain an Owner target, change its policy to Merge and wait until the resource reports Ready=True for the new generation.
  4. Delete each PsonoSecret and wait until it is gone.
  5. Uninstall the operator only after all resources have been handled.

Deleting an Owner PsonoSecret removes its target. Deleting a Merge PsonoSecret leaves the target and its synchronized values.

Remove the operator with:

helm uninstall psono-kubernetes-operator \
  --namespace psono-kubernetes-operator-system

Helm does not automatically remove CRDs installed through a chart's crds directory. Remove psonosecrets.secrets.psono.com manually only after all PsonoSecret resources have been handled and no installation still uses it.

# PsonoSecret field reference

Field Required Default Description
spec.refreshInterval No 5m Poll interval. Minimum 30s.
spec.authentication.secretRef.name Yes Same-namespace authentication Secret. Must differ from spec.target.name.
spec.authentication.secretRef.serverUrlKey No serverUrl Key containing the Psono server URL.
spec.authentication.secretRef.apiKeyIdKey No apiKeyId Key containing the restricted API key ID.
spec.authentication.secretRef.apiSecretKeyKey No apiSecretKey Key containing the API secret key.
spec.authentication.secretRef.caBundleKey No Key containing a PEM CA bundle.
spec.target.name Yes Name of the target Kubernetes Secret in the same namespace. Must differ from the authentication Secret name.
spec.target.creationPolicy No Owner Owner or Merge.
spec.target.type No Opaque Kubernetes Secret type.
spec.data Yes One or more explicit value mappings.
spec.data[].secretKey Yes Key written to the target Kubernetes Secret.
spec.data[].remoteRef.secretId Yes Psono SECRET_ID assigned to the restricted API key.
spec.data[].remoteRef.environmentVariable Conditional Exact key in a Psono Environment Variables entry.
spec.data[].remoteRef.field Conditional Dot-separated string field path in another Psono entry.

Exactly one of environmentVariable and field is required in each mapping.