Skip to content

Secret Management

Overview

The Nextcloud operator supports referencing existing Kubernetes secrets for sensitive credentials instead of embedding them in the Nextcloud CR. This follows security best practices and integrates seamlessly with secret management tools like:

  • External Secrets Operator (ESO)
  • Sealed Secrets
  • HashiCorp Vault
  • AWS Secrets Manager / Azure Key Vault / Google Secret Manager
  • Manual secret creation

How It Works

Priority Order

For each credential type, the operator follows this priority:

  1. credentialsSecret — Reference to existing Kubernetes secret (highest priority)
  2. Inline values — Credentials defined directly in the CR
  3. Defaults — Built-in defaults (admin credentials only)

Benefits

  • Security: Credentials never stored in CRDs or Git
  • Separation of Concerns: Dev team manages apps, ops team manages secrets
  • GitOps Friendly: Nextcloud CRs can be in Git without exposing secrets
  • Integration: Works with any secret management tool
  • Flexibility: Mix and match — use secrets for some, inline for others

Supported Credential Types

1. Database Credentials

spec:
  database:
    type: postgresql
    credentialsSecret: my-db-credentials

Required secret keys: host, name, user, password

Optional secret keys: port (default: 5432), type (default: postgresql)

2. Redis Credentials

spec:
  redis:
    enabled: true
    credentialsSecret: my-redis-credentials

Required secret keys: host

Optional secret keys: port (default: 6379), password

3. S3 Credentials

spec:
  s3:
    enabled: true
    credentialsSecret: my-s3-credentials

Required secret keys: bucket, accessKey, secretKey

Optional secret keys: endpoint (default: s3.amazonaws.com), region (default: us-east-1)

4. Admin Credentials

spec:
  admin:
    credentialsSecret: my-admin-credentials

Required secret keys: username, password

Optional secret keys: email

5. Mail/SMTP Credentials

spec:
  mail:
    enabled: true
    credentialsSecret: my-mail-credentials

Secret keys: fromAddress, domain, smtpHost, smtpPort, smtpSecure, smtpAuthType, smtpName, smtpPassword

6. OIDC Credentials

OIDC uses a different pattern — clientSecretRef references a single key in a secret. The recommended form is to use clientSecretRef directly:

spec:
  oidc:
    enabled: true
    providerName: keycloak
    clientId: nextcloud
    clientSecretRef:
      name: oidc-secret
      key: client-secret  # optional, defaults to "client-secret"
    discoveryUri: https://iam.example.com/realms/office/.well-known/openid-configuration

OIDC client secret auto-materialization

As a convenience for non-GitOps flows, the operator also accepts an inline clientSecret and automatically promotes it to an operator-owned Kubernetes Secret:

  1. On the first reconcile, the operator reads spec.oidc.clientSecret.
  2. It creates (or updates) an operator-owned Secret named <instance>-nextcloud-oidc in the instance namespace, with key client-secret.
  3. It scrubs the inline value from the CR spec (clientSecret"") and writes back clientSecretRef pointing at the new Secret — so the plaintext value no longer lives in etcd or audit logs.
  4. All subsequent reconciles resolve the secret from clientSecretRef, never from the CR spec.

The materialized Secret name and key:

Field Value
Secret name <instance-name>-nextcloud-oidc
Secret key client-secret

The Secret is registered in status.secrets.oidc so it is garbage-collected when the instance is deleted.

The OIDCReady status condition reflects whether the provider was successfully configured:

status:
  secrets:
    oidc: my-instance-nextcloud-oidc
  conditions:
    - type: OIDCReady
      status: "True"
      reason: Configured
      message: "OIDC provider 'keycloak' configured successfully."

Possible reason values:

Reason Meaning
Configured Provider configured via occ user_oidc:provider
IncompleteConfig clientId or discoveryUri missing
SecretResolutionFailed Neither clientSecretRef nor inline clientSecret resolved
ConfigureFailed occ command returned non-zero

GitOps / Flux exception

If the NextcloudInstance CR carries Flux Kustomize labels (kustomize.toolkit.fluxcd.io/* or helm.toolkit.fluxcd.io/*), the spec scrub is skipped. Flux would immediately revert any operator-applied spec change from its git source, causing an endless patch/revert loop. The operator-owned Secret is still created and used to configure the provider, but clientSecret is left in the spec. Manage the scrub on the Flux/git side instead.

Rotating the OIDC client secret

See Rotating the OIDC client secret in the runbook section below.

Automatic sanitization of inline mail / S3 / database credentials

(since v0.16.0) The same scrub-on-reconcile behaviour now applies to the other plaintext credential fields. When you supply a credential inline, the operator promotes it into the section's operator-owned Secret (which it already creates) and scrubs the spec to a credentialsSecret reference — so the plaintext no longer persists in the CR (kubectl get nextcloud -o yaml), etcd, or later audit reads.

Inline spec field Scrubbed into Secret Secret key(s) status.secrets
spec.mail.smtpPassword <instance>-nextcloud-mail smtp-password mail
spec.s3.accessKey / spec.s3.secretKey <instance>-nextcloud-s3 s3-access-key / s3-secret-key s3
spec.database.password <instance>-nextcloud-db db-password database

After the scrub, spec.<section>.credentialsSecret is set and the inline field is emptied; subsequent reconciles resolve from the Secret, which is garbage-collected with the instance.

Notes:

  • Accepted window: the scrub runs on the operator's first reconcile, so plaintext is briefly present between kubectl apply and that reconcile (and in the create-event audit log). To avoid the window entirely, provide a credentialsSecret yourself at creation time — the operator then never sees plaintext.
  • Managed PostgreSQL is never scrubbed. With spec.database.managed: true the password is operator-generated (the Percona Secret); there is no user plaintext to scrub.
  • GitOps / Flux exception applies identically — see above. On Flux-managed CRs the Secret is still used but the spec scrub is skipped (it would cause a patch/revert loop); sanitize on the git side instead.
  • Rotation: edit the operator-owned Secret directly (e.g. kubectl edit secret <instance>-nextcloud-mail); the operator reconciles it on the next pass.

Examples

All Credentials from Secrets

apiVersion: k8s.bnerd.com/v1alpha1
kind: NextcloudInstance
metadata:
  name: secure-nextcloud
spec:
  profile: production

  database:
    type: postgresql
    credentialsSecret: nextcloud-db-creds

  redis:
    enabled: true
    credentialsSecret: nextcloud-redis-creds

  s3:
    enabled: true
    credentialsSecret: nextcloud-s3-creds

  admin:
    credentialsSecret: nextcloud-admin-creds

Mixed Approach

apiVersion: k8s.bnerd.com/v1alpha1
kind: NextcloudInstance
metadata:
  name: mixed-nextcloud
spec:
  # Database from secret (production, managed by ops)
  database:
    type: postgresql
    credentialsSecret: prod-db-credentials

  # Redis inline (development, not sensitive)
  redis:
    enabled: true
    host: redis.default.svc
    port: 6379

  # Admin from secret
  admin:
    credentialsSecret: admin-credentials

With External Secrets Operator

# External Secret that syncs from Vault
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: nextcloud-db-credentials
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: nextcloud-db-credentials
    creationPolicy: Owner
  dataFrom:
    - extract:
        key: secret/data/nextcloud/database

---
# Nextcloud references the synced secret
apiVersion: k8s.bnerd.com/v1alpha1
kind: NextcloudInstance
metadata:
  name: my-nextcloud
spec:
  database:
    type: postgresql
    credentialsSecret: nextcloud-db-credentials

With Sealed Secrets

# Sealed Secret (safe to commit to Git)
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: nextcloud-admin-credentials
spec:
  encryptedData:
    username: AgBvN3RI...
    password: AgCd8hP2...
  template:
    metadata:
      name: nextcloud-admin-credentials

---
# Nextcloud references the sealed secret
apiVersion: k8s.bnerd.com/v1alpha1
kind: NextcloudInstance
metadata:
  name: my-nextcloud
spec:
  admin:
    credentialsSecret: nextcloud-admin-credentials

Creating Secrets

With kubectl

# Database
kubectl create secret generic nextcloud-db-creds \
  --from-literal=host=postgres.db.svc \
  --from-literal=port=5432 \
  --from-literal=name=nextcloud \
  --from-literal=user=nextcloud \
  --from-literal=password='db-password'

# Redis
kubectl create secret generic nextcloud-redis-creds \
  --from-literal=host=redis.cache.svc \
  --from-literal=port=6379 \
  --from-literal=password='redis-password'

# S3
kubectl create secret generic nextcloud-s3-creds \
  --from-literal=bucket=my-bucket \
  --from-literal=endpoint=s3.amazonaws.com \
  --from-literal=region=us-east-1 \
  --from-literal=accessKey='AKIA...' \
  --from-literal=secretKey='secret...'

# Admin
kubectl create secret generic nextcloud-admin-creds \
  --from-literal=username=admin \
  --from-literal=password='admin-password' \
  --from-literal=email=admin@example.com

With YAML Manifest

apiVersion: v1
kind: Secret
metadata:
  name: nextcloud-db-credentials
type: Opaque
stringData:
  host: postgres.database.svc.cluster.local
  port: "5432"
  name: nextcloud
  user: nextcloud
  password: "super-secret-password"

Warning

Do not commit unencrypted Secret manifests to Git. Use Sealed Secrets, External Secrets Operator, or SOPS instead.

Error Handling

Secret Not Found

If you specify a credentialsSecret that doesn't exist, the instance will fail:

status:
  phase: Failed
  conditions:
    - type: SecretsReady
      status: "False"
      reason: "Failed"
      message: "Database credentialsSecret 'nonexistent-secret' specified but secret not found"

Missing Required Keys

If a secret exists but is missing required keys:

Secret default/my-db-creds is missing required keys: password

Security Best Practices

  1. Always use credentialsSecret for production — never commit inline passwords
  2. Integrate with secret management tools — External Secrets Operator + Vault/AWS/Azure
  3. Restrict RBAC for secrets — limit who can read secrets
  4. Rotate credentials regularly — update secrets and trigger reconciliation
  5. Use separate secrets per environment — production and staging should use different credentials

Runbook: rotating the OIDC client secret

When you rotate the OIDC client secret at your identity provider, update the operator-owned Secret and trigger a reconcile:

  1. Update the operator-owned Secret with the new value:

    kubectl patch secret <instance>-nextcloud-oidc \
      -n <instance-namespace> \
      --type=json \
      -p '[{"op":"replace","path":"/data/client-secret","value":"'"$(echo -n '<new-secret>' | base64 -w0)"'"}]'
    

  2. Force-reconcile by applying the force-reconcile annotation:

    kubectl annotate nextcloudinstance <instance> \
      -n <instance-namespace> \
      k8s.bnerd.com/force-reconcile="$(date +%s)"
    

  3. Verify the OIDCReady condition is True:

    kubectl get nextcloudinstance <instance> -n <instance-namespace> \
      -o jsonpath='{.status.conditions[?(@.type=="OIDCReady")]}'
    

If the condition shows SecretResolutionFailed, check that the Secret exists in the correct namespace and contains the client-secret key.

Troubleshooting

# Check secret exists
kubectl get secret nextcloud-db-credentials

# List keys in secret
kubectl get secret nextcloud-db-credentials -o json | jq -r '.data | keys | .[]'

# Check operator logs for credential messages
kubectl logs -n nextcloud-operator-system -l app.kubernetes.io/name=nextcloud-operator | grep -i credential

# Inspect the OIDC-owned secret for an instance
kubectl get secret <instance>-nextcloud-oidc -n <instance-namespace>

# Check OIDC condition
kubectl get nextcloudinstance <instance> -n <instance-namespace> \
  -o jsonpath='{.status.conditions[?(@.type=="OIDCReady")]}'