# 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
- Requirements
- Prepare Psono
- Install the operator
- Create the authentication Secret
- Synchronize an Environment Variables entry
- Synchronize fields from other entry types
- Target creation policies
- Consume the generated Secret
- Verify synchronization
- Troubleshooting
- Security considerations
- Upgrade or remove the operator
- PsonoSecret field reference
- Related documentation
# 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:
- Reads the authentication Secret from the
PsonoSecretnamespace. - Calls Psono's sessionless
/api-key-access/secret/endpoint. - Receives the encrypted entry and its encrypted per-entry key.
- Decrypts both layers locally using XSalsa20-Poly1305.
- Extracts only the values listed in the
PsonoSecret. - Creates or updates the target Kubernetes Secret.
- 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:
- Create a dedicated API key for one application, namespace, or equivalent trust boundary.
- Grant read access only.
- Assign only the Psono entries required by that workload.
- Leave "Allow insecure usage" disabled.
- Record the API key ID and API secret key.
- Record the
SECRET_IDshown 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
PsonoSecretresources. - Treat permission to create a
PsonoSecretas 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
datavalues 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
PsonoSecretfields, 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:
- List all resources with
kubectl get psonosecrets --all-namespaces. - Decide whether each target should be deleted or retained.
- To retain an
Ownertarget, change its policy toMergeand wait until the resource reportsReady=Truefor the new generation. - Delete each
PsonoSecretand wait until it is gone. - 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.