Skip to content

feat: skip opencloud init — inject all secrets as runtime ENV vars#58

Open
bernardgut wants to merge 1 commit intoTim-herbie:mainfrom
bernardgut:feat/externalize-init-secrets
Open

feat: skip opencloud init — inject all secrets as runtime ENV vars#58
bernardgut wants to merge 1 commit intoTim-herbie:mainfrom
bernardgut:feat/externalize-init-secrets

Conversation

@bernardgut
Copy link
Copy Markdown

@bernardgut bernardgut commented Mar 15, 2026

Summary

Removes opencloud init from the container entrypoint entirely. All secrets and UUIDs are now injected directly as runtime environment variables from a Kubernetes Secret, making deployments fully stateless and restart-safe.

This eliminates:

  • Init-time config generation and its write-to-disk requirement
  • Secret regeneration on pod restart (the root cause of LDAP bind failures)
  • The need for a custom binary or upstream patches

Works with the standard upstream opencloudeu/opencloud-rolling image — no modifications needed.

Changes

templates/opencloud/init-secrets.yaml

  • Generates 16 keys: 11 random secrets + 5 stable UUIDs
  • Persisted across Helm upgrades via lookup + helm.sh/resource-policy: keep

templates/opencloud/deployment.yaml

  • Removed opencloud init || true from startup command
  • Injects 24 runtime ENV vars from the init secret:
    • Core secrets: OC_JWT_SECRET, OC_MACHINE_AUTH_API_KEY, OC_TRANSFER_SECRET, OC_URL_SIGNING_SECRET, OC_SYSTEM_USER_API_KEY
    • Service account: OC_SERVICE_ACCOUNT_ID, OC_SERVICE_ACCOUNT_SECRET, SETTINGS_SERVICE_ACCOUNT_IDS
    • IDM passwords: IDM_ADMIN_PASSWORD, IDM_SVC_PASSWORD, IDM_REVASVC_PASSWORD, IDM_IDPSVC_PASSWORD
    • LDAP bind (per-service): USERS_LDAP_BIND_PASSWORD, GROUPS_LDAP_BIND_PASSWORD, AUTH_BASIC_LDAP_BIND_PASSWORD, IDP_LDAP_BIND_PASSWORD, GRAPH_LDAP_BIND_PASSWORD
    • UUIDs: OC_SYSTEM_USER_ID, OC_ADMIN_USER_ID, GRAPH_APPLICATION_ID, STORAGE_USERS_MOUNT_ID, GATEWAY_STORAGE_USERS_MOUNT_ID
    • Other: COLLABORATION_WOPI_SECRET, THUMBNAILS_TRANSFER_TOKEN

README.md

  • Updated Init Secrets section documenting the skip-init architecture

Testing

Tested with opencloudeu/opencloud-rolling:latest on a live Kubernetes cluster:

  • All 5 pods start and run (opencloud, keycloak, collabora, collaboration, tika)
  • Internal service authentication works (storage-system authenticates with injected UUIDs)
  • Web UI serves correctly
  • WOPI discovery between collaboration ↔ collabora works
  • Pod restarts maintain stable secrets (no LDAP bind failures)

Related

@bernardgut bernardgut changed the title feat: externalize runtime secrets via initSecrets feat: externalize init secrets via environment variables Mar 16, 2026
@bernardgut bernardgut marked this pull request as ready for review March 16, 2026 05:50
Copilot AI review requested due to automatic review settings March 16, 2026 05:50
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a Helm-managed “init secrets” Secret intended to make opencloud init credentials deterministic across pod restarts (and stable across Helm upgrades), and wires those secrets into the OpenCloud Deployment via environment variables.

Changes:

  • Introduces a new *-init Secret template that persists (via helm.sh/resource-policy: keep + lookup) and contains 12 generated secret values.
  • Injects init-secret keys as env vars into the OpenCloud Deployment and adjusts startup behavior (banned-password list file creation) plus switches probes to tcpSocket.
  • Adds opencloud.initSecrets.existingSecret to allow using a pre-created Secret instead of auto-generating one.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
charts/opencloud/values.yaml Adds opencloud.initSecrets.existingSecret configuration knob.
charts/opencloud/templates/opencloud/init-secrets.yaml New template to create/retain the init Secret and reuse values via lookup.
charts/opencloud/templates/opencloud/deployment.yaml Injects init-secret values as env vars; changes startup command and probe types.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@bernardgut bernardgut force-pushed the feat/externalize-init-secrets branch from a6eb37b to 16ec875 Compare March 16, 2026 08:31
@bernardgut
Copy link
Copy Markdown
Author

bernardgut commented Mar 16, 2026

This is ready. But do not merge yet. Wait for upstream to review & merge opencloud-eu/opencloud#2484.
Thanks

Remove opencloud init from startup command. All secrets and UUIDs are
now injected directly as runtime ENV vars from Kubernetes Secrets.
This eliminates the need for a custom binary or config persistence.

Changes:
- Remove opencloud init from container entrypoint
- init-secrets.yaml: add 5 UUIDs alongside 11 secrets (16 keys total)
- deployment.yaml: inject 24 runtime ENV vars (secrets, UUIDs, LDAP
  bind passwords, IDM admin, service account IDs)
- README.md: update Init Secrets section

Tested with upstream opencloudeu/opencloud-rolling:latest image.
All 5 pods (opencloud, keycloak, collabora, collaboration, tika) start
and run successfully without opencloud init.

Closes: opencloud-eu/opencloud#2483
@bernardgut bernardgut force-pushed the feat/externalize-init-secrets branch from 82b4c1d to 13734f0 Compare March 16, 2026 10:27
@bernardgut bernardgut changed the title feat: externalize init secrets via environment variables feat: skip opencloud init — inject all secrets as runtime ENV vars Mar 16, 2026
@bernardgut
Copy link
Copy Markdown
Author

bernardgut commented Mar 16, 2026

After discussion with the upstream OpenCloud maintainer in opencloud-eu/opencloud#2483, we changed our approach.

Before (ENV-aware init)

The original version of this PR made opencloud init respect environment variables so the generated config would match the runtime expectations. This required a custom binary patch to the upstream OpenCloud server.

After (skip init)

The maintainer confirmed that all values opencloud init writes can be set directly as runtime environment variables. We validated this end-to-end:

  1. Removed opencloud init || true from the entrypoint
  2. Mapped all 24 runtime ENV vars from the upstream Go config structs
  3. Added 5 UUIDs to the chart-generated secret (previously only random strings)
  4. Added per-service LDAP bind password mapping (the non-obvious piece)
  5. Tested with the stock opencloudeu/opencloud-rolling:latest image — all 5 pods start and run

This makes the upstream PR opencloud-eu/opencloud#2484 unnecessary. No custom image or binary patch is needed.

This is now ready I think

@bernardgut
Copy link
Copy Markdown
Author

Are you interested in this one ? @Tim-herbie

@Tim-herbie
Copy link
Copy Markdown
Owner

@bernardgut Let me take a look at it over the weekend.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the OpenCloud Helm chart to remove opencloud init from the container startup flow and instead provide all required internal credentials (secrets + stable UUIDs) via a persisted Kubernetes Secret injected as runtime environment variables, aiming for stateless/restart-safe deployments.

Changes:

  • Add opencloud.initSecrets.existingSecret to support either a user-managed Secret or an auto-generated *-init Secret.
  • Introduce a new *-init Secret template that persists across upgrades via lookup and helm.sh/resource-policy: keep.
  • Update the OpenCloud Deployment to drop opencloud init and inject the runtime credentials from the init Secret; adjust probes to TCP checks.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
charts/opencloud/values.yaml Adds values for configuring an externally-managed vs auto-generated init Secret.
charts/opencloud/templates/opencloud/init-secrets.yaml New template to generate/persist the init credentials Secret using lookup + keep policy.
charts/opencloud/templates/opencloud/deployment.yaml Removes opencloud init, injects env vars from the init Secret, and changes probes to TCP.
charts/opencloud/README.md Documents the init-secrets / skip-init architecture and the new values option.
Comments suppressed due to low confidence (1)

charts/opencloud/templates/opencloud/deployment.yaml:487

  • Switching the OpenCloud probes from HTTP /health to a plain TCP check makes readiness/liveness less accurate (port-open doesn’t guarantee the service is healthy). This is also inconsistent with charts/opencloud/templates/collaboration/deployment.yaml, where the wait-for-opencloud initContainer still waits on http://...:9200/health. Consider keeping HTTP probes for OpenCloud, or update dependent components to align on TCP (and ensure /health availability if it remains a dependency).
          startupProbe:
            tcpSocket:
              port: 9200
            periodSeconds: 2
            timeoutSeconds: 5
            failureThreshold: 60
          livenessProbe:
            tcpSocket:
              port: 9200
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
          readinessProbe:
            tcpSocket:
              port: 9200
            periodSeconds: 5
            timeoutSeconds: 5
            failureThreshold: 3

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

imagePullPolicy: {{ include "opencloud.image.pullPolicy" (dict "pullPolicy" .Values.image.pullPolicy "global" .Values.global) }}
command: ["/bin/sh"]
args: ["-c", "opencloud init || true; opencloud server"]
args: ["-c", "mkdir -p /var/lib/opencloud/.opencloud/config && touch /var/lib/opencloud/.opencloud/config/banned-password-list.txt; opencloud server"]
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The container startup command touches an empty /var/lib/opencloud/.opencloud/config/banned-password-list.txt, but the chart also mounts a non-empty banned-password-list.txt configmap at /etc/opencloud/banned-password-list.txt and sets OC_PASSWORD_POLICY_BANNED_PASSWORDS_LIST to the relative filename. Depending on how the binary resolves the relative path, this can cause it to read the empty touched file instead of the intended list from the ConfigMap, weakening the password policy. Prefer pointing OC_PASSWORD_POLICY_BANNED_PASSWORDS_LIST to the mounted absolute path, or copy the mounted file into the expected config directory rather than creating an empty one.

Suggested change
args: ["-c", "mkdir -p /var/lib/opencloud/.opencloud/config && touch /var/lib/opencloud/.opencloud/config/banned-password-list.txt; opencloud server"]
args: ["-c", "mkdir -p /var/lib/opencloud/.opencloud/config && if [ -f /etc/opencloud/banned-password-list.txt ]; then cp /etc/opencloud/banned-password-list.txt /var/lib/opencloud/.opencloud/config/banned-password-list.txt; else touch /var/lib/opencloud/.opencloud/config/banned-password-list.txt; fi; opencloud server"]

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bernardgut Is that okay?

Comment on lines +17 to +32
jwtSecret: {{ index $existingSecret.data "jwtSecret" | default (randAlphaNum 32 | b64enc) }}
machineAuthApiKey: {{ index $existingSecret.data "machineAuthApiKey" | default (randAlphaNum 32 | b64enc) }}
transferSecret: {{ index $existingSecret.data "transferSecret" | default (randAlphaNum 32 | b64enc) }}
serviceAccountSecret: {{ index $existingSecret.data "serviceAccountSecret" | default (randAlphaNum 32 | b64enc) }}
idmServicePassword: {{ index $existingSecret.data "idmServicePassword" | default (randAlphaNum 32 | b64enc) }}
idmRevaServicePassword: {{ index $existingSecret.data "idmRevaServicePassword" | default (randAlphaNum 32 | b64enc) }}
idmIdpServicePassword: {{ index $existingSecret.data "idmIdpServicePassword" | default (randAlphaNum 32 | b64enc) }}
collaborationWopiSecret: {{ index $existingSecret.data "collaborationWopiSecret" | default (randAlphaNum 32 | b64enc) }}
systemUserApiKey: {{ index $existingSecret.data "systemUserApiKey" | default (randAlphaNum 32 | b64enc) }}
urlSigningSecret: {{ index $existingSecret.data "urlSigningSecret" | default (randAlphaNum 32 | b64enc) }}
thumbnailsTransferSecret: {{ index $existingSecret.data "thumbnailsTransferSecret" | default (randAlphaNum 32 | b64enc) }}
systemUserID: {{ index $existingSecret.data "systemUserID" | default (uuidv4 | b64enc) }}
adminUserID: {{ index $existingSecret.data "adminUserID" | default (uuidv4 | b64enc) }}
serviceAccountID: {{ index $existingSecret.data "serviceAccountID" | default (uuidv4 | b64enc) }}
graphApplicationID: {{ index $existingSecret.data "graphApplicationID" | default (uuidv4 | b64enc) }}
storageUsersMountID: {{ index $existingSecret.data "storageUsersMountID" | default (uuidv4 | b64enc) }}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the $existingSecret branch, the generated/retained base64 values under data: are emitted without | quote. Unquoted scalars can be parsed as non-strings by YAML (e.g., numeric-looking values), which Kubernetes will reject for Secret .data fields. For consistency with the else branch and to avoid type surprises, quote these values as well.

Suggested change
jwtSecret: {{ index $existingSecret.data "jwtSecret" | default (randAlphaNum 32 | b64enc) }}
machineAuthApiKey: {{ index $existingSecret.data "machineAuthApiKey" | default (randAlphaNum 32 | b64enc) }}
transferSecret: {{ index $existingSecret.data "transferSecret" | default (randAlphaNum 32 | b64enc) }}
serviceAccountSecret: {{ index $existingSecret.data "serviceAccountSecret" | default (randAlphaNum 32 | b64enc) }}
idmServicePassword: {{ index $existingSecret.data "idmServicePassword" | default (randAlphaNum 32 | b64enc) }}
idmRevaServicePassword: {{ index $existingSecret.data "idmRevaServicePassword" | default (randAlphaNum 32 | b64enc) }}
idmIdpServicePassword: {{ index $existingSecret.data "idmIdpServicePassword" | default (randAlphaNum 32 | b64enc) }}
collaborationWopiSecret: {{ index $existingSecret.data "collaborationWopiSecret" | default (randAlphaNum 32 | b64enc) }}
systemUserApiKey: {{ index $existingSecret.data "systemUserApiKey" | default (randAlphaNum 32 | b64enc) }}
urlSigningSecret: {{ index $existingSecret.data "urlSigningSecret" | default (randAlphaNum 32 | b64enc) }}
thumbnailsTransferSecret: {{ index $existingSecret.data "thumbnailsTransferSecret" | default (randAlphaNum 32 | b64enc) }}
systemUserID: {{ index $existingSecret.data "systemUserID" | default (uuidv4 | b64enc) }}
adminUserID: {{ index $existingSecret.data "adminUserID" | default (uuidv4 | b64enc) }}
serviceAccountID: {{ index $existingSecret.data "serviceAccountID" | default (uuidv4 | b64enc) }}
graphApplicationID: {{ index $existingSecret.data "graphApplicationID" | default (uuidv4 | b64enc) }}
storageUsersMountID: {{ index $existingSecret.data "storageUsersMountID" | default (uuidv4 | b64enc) }}
jwtSecret: {{ index $existingSecret.data "jwtSecret" | default (randAlphaNum 32 | b64enc) | quote }}
machineAuthApiKey: {{ index $existingSecret.data "machineAuthApiKey" | default (randAlphaNum 32 | b64enc) | quote }}
transferSecret: {{ index $existingSecret.data "transferSecret" | default (randAlphaNum 32 | b64enc) | quote }}
serviceAccountSecret: {{ index $existingSecret.data "serviceAccountSecret" | default (randAlphaNum 32 | b64enc) | quote }}
idmServicePassword: {{ index $existingSecret.data "idmServicePassword" | default (randAlphaNum 32 | b64enc) | quote }}
idmRevaServicePassword: {{ index $existingSecret.data "idmRevaServicePassword" | default (randAlphaNum 32 | b64enc) | quote }}
idmIdpServicePassword: {{ index $existingSecret.data "idmIdpServicePassword" | default (randAlphaNum 32 | b64enc) | quote }}
collaborationWopiSecret: {{ index $existingSecret.data "collaborationWopiSecret" | default (randAlphaNum 32 | b64enc) | quote }}
systemUserApiKey: {{ index $existingSecret.data "systemUserApiKey" | default (randAlphaNum 32 | b64enc) | quote }}
urlSigningSecret: {{ index $existingSecret.data "urlSigningSecret" | default (randAlphaNum 32 | b64enc) | quote }}
thumbnailsTransferSecret: {{ index $existingSecret.data "thumbnailsTransferSecret" | default (randAlphaNum 32 | b64enc) | quote }}
systemUserID: {{ index $existingSecret.data "systemUserID" | default (uuidv4 | b64enc) | quote }}
adminUserID: {{ index $existingSecret.data "adminUserID" | default (uuidv4 | b64enc) | quote }}
serviceAccountID: {{ index $existingSecret.data "serviceAccountID" | default (uuidv4 | b64enc) | quote }}
graphApplicationID: {{ index $existingSecret.data "graphApplicationID" | default (uuidv4 | b64enc) | quote }}
storageUsersMountID: {{ index $existingSecret.data "storageUsersMountID" | default (uuidv4 | b64enc) | quote }}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bernardgut Is that okay?

Comment on lines +332 to +337
key: systemUserID
- name: OC_ADMIN_USER_ID
valueFrom:
secretKeyRef:
name: {{ $initSecretName }}
key: adminUserID
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OC_ADMIN_USER_ID is defined twice when OIDC is enabled: earlier in this template it is explicitly set to an empty string under the OIDC block, but this new runtime-secrets section always injects it from the init Secret. Duplicate env var names are error-prone and the later entry will override the earlier one, potentially changing OIDC behavior. Consider only injecting OC_ADMIN_USER_ID when OIDC is disabled, or removing the earlier empty assignment if it’s no longer required.

Suggested change
key: systemUserID
- name: OC_ADMIN_USER_ID
valueFrom:
secretKeyRef:
name: {{ $initSecretName }}
key: adminUserID
key: systemUserID
{{- if not .Values.opencloud.oidc.enabled }}
- name: OC_ADMIN_USER_ID
valueFrom:
secretKeyRef:
name: {{ $initSecretName }}
key: adminUserID
{{- end }}

Copilot uses AI. Check for mistakes.
@Tim-herbie
Copy link
Copy Markdown
Owner

With the current state, the helm installations fails, because of duplicate ENVs, did you test that already?

Run helm install opencloud-test charts/opencloud \
  helm install opencloud-test charts/opencloud \
    --namespace opencloud-test \
    --create-namespace \
    --wait \
    --timeout 5m
  shell: /usr/bin/bash -e {0}
Error: INSTALLATION FAILED: failed to create typed patch object (opencloud-test/opencloud-test-opencloud; apps/v1, Kind=Deployment): errors:
  .spec.template.spec.containers[name="opencloud"].env: duplicate entries for key [name="OC_ADMIN_USER_ID"]
  .spec.template.spec.containers[name="opencloud"].env: duplicate entries for key [name="IDM_ADMIN_PASSWORD"]

@bernardgut
Copy link
Copy Markdown
Author

Ok let me take another look over this week

tosih added a commit to tosih/home-ops that referenced this pull request Mar 26, 2026
…e ENV vars

- Remove init container completely
- Create ExternalSecret with 16 keys (11 secrets + 5 UUIDs)
- Inject all 24 runtime ENV vars from init secret
- Skip opencloud init entirely - no config generation needed
- Based on PR: Tim-herbie/opencloud-helm#58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

opencloud init: support environment variables for all generated secrets

3 participants