diff --git a/apps/kustomization.yaml b/apps/kustomization.yaml index b6aaf17..bf75f1e 100644 --- a/apps/kustomization.yaml +++ b/apps/kustomization.yaml @@ -17,6 +17,10 @@ resources: - qbittorrent.yaml - jackett.yaml - seerr.yaml + - nextcloud.yaml + - nextcloud-db.yaml # CNPG PostgreSQL Cluster (ns nextcloud-db) — first CNPG-DB app + - nextcloud-db-app.sops.yaml # SOPS: CNPG owner credentials (basic-auth) for initdb + - nextcloud-bootstrap-secret.sops.yaml # SOPS: break-glass admin + mirrored db-password (ns nextcloud) # Gitea's headless-install env + break-glass admin postStart hook are now first-class in the # ServiceDefinition (catalog/gitea.yaml env/lifecycle, schema >=1.2.0) and rendered inline into # apps/gitea.yaml, so the former gitea-bootstrap-patch.yaml strategic-merge patch is retired. diff --git a/apps/nextcloud-bootstrap-secret.sops.yaml b/apps/nextcloud-bootstrap-secret.sops.yaml new file mode 100644 index 0000000..11e4d07 --- /dev/null +++ b/apps/nextcloud-bootstrap-secret.sops.yaml @@ -0,0 +1,36 @@ +# Nextcloud break-glass admin + DB password (WS-APPS, Phase 6). SOPS-encrypted (age). +# admin-*: break-glass local admin (docs/15 availability caveat), created on first-boot install. +# db-password: MIRRORS nextcloud-db-app/password (ns nextcloud-db) so the app can read the DB +# credential from its own namespace. Recover: infra/devcontainer/dev.sh run sops -d apps/nextcloud-bootstrap-secret.sops.yaml +apiVersion: v1 +kind: Secret +metadata: + name: nextcloud-bootstrap + namespace: nextcloud + labels: + app.kubernetes.io/name: nextcloud +type: Opaque +stringData: + admin-username: ENC[AES256_GCM,data:GVvF2Q6ymg==,iv:z0aNezBuAit4nZU0o8C5vd0V0eqFV27Ly6BX6EHA/QQ=,tag:mX17Bc5uC/HMH1d4uA3WGw==,type:str] + admin-password: ENC[AES256_GCM,data:kdP3lOEMKTOrefDNYY0m9b2JsVEWF0cwYHhpizFRvN0tA8nYv0blgvYmS16KvuUI,iv:9bbntbUewcayR3RgdeieThaFxt0UwFYHIqQoD1VSHHo=,tag:E1DHYb5TuNLl/WVFRuDCtw==,type:str] + db-password: ENC[AES256_GCM,data:Xh62RMSTEv9uS4UYhL63qclbRWFhHtUZn9/YCEbBHwkYtImYNSgKltt9d5ntUeKk,iv:ITCJE3J1eSdIzBEnWh3q2BGW3O82fqqRZ+GnWodFh78=,tag:f9MsOPPTec3dGCXweFUetQ==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age175l8mdpdfu7hq0aml3exs3gykarwjmuvr23sq56s3wjejnyk44wsp4mfzg + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3bVlRZXNLZmZROHQ2czM0 + OTdXQzczTmcvYWlMdDJDQkttMnJwZ21Ka2djCjZpMTN5WUxCcDhOT0Z2bnk3eXU0 + aHg3N3Vvdk9Yc2xSNXJFamZLMnVsYkEKLS0tIEJGK2xyd2lKVjdXOERCMURIR2ZV + SDdoWXhVRi9DR0RzL0dlM0dtcTlFQlEKNVBukqVCZ7ZjREYAl2xK/YxuJVKEgESh + ToKuzCgydEaxW3NoKWMvZxJUM5vnGI1rPxSZdGgXDjcpI4Q4iTYmZA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-08-26T06:30:08Z" + mac: ENC[AES256_GCM,data:INbmOvfiZFll97zz3Dfq4xFpbBWpNUoW4LOERUSQ2jBy2UBGO8wSNf0r8F328mlO1GqFXFs0a0LD11pDgC2wAhF/bQWYsPFAYNEQFgU3FpxCNnLNPUN9nS58CE9C1sMIs5WaAaOzFBudRXEYulQNRKuLPGOF7UKytxkzmgIggT4=,iv:JuzQ7scb+CQg0kOtwzTqNe0D8V90sQSpiNA/2lMsJr8=,tag:1ICb7zPIqvonmEgEfYdGqA==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.9.4 diff --git a/apps/nextcloud-db-app.sops.yaml b/apps/nextcloud-db-app.sops.yaml new file mode 100644 index 0000000..7a24f40 --- /dev/null +++ b/apps/nextcloud-db-app.sops.yaml @@ -0,0 +1,36 @@ +# Nextcloud PostgreSQL owner credentials (WS-APPS, Phase 6). SOPS-encrypted (age). +# type basic-auth (keys username/password) so the CNPG nextcloud-db Cluster consumes it as the +# nextcloud role via spec.bootstrap.initdb.secret. The SAME password is mirrored (out of band) into +# nextcloud-bootstrap/db-password in ns nextcloud for the app env (cross-ns Secret reads are not +# possible). Recover: infra/devcontainer/dev.sh run sops -d apps/nextcloud-db-app.sops.yaml +apiVersion: v1 +kind: Secret +metadata: + name: nextcloud-db-app + namespace: nextcloud-db + labels: + app.kubernetes.io/name: nextcloud-db +type: kubernetes.io/basic-auth +stringData: + username: ENC[AES256_GCM,data:fB2YI9sNjeZj,iv:o1L0XH5bMOPODdk4cFxMMTOOCQNhAPm9aI708x4FoCE=,tag:nKrVmibWaPeJTnFN84BlSQ==,type:str] + password: ENC[AES256_GCM,data:09mWJL4t7yOoYsMFsoPNib7wgPJKQ3vmQudDu4+hQwDOReZTA3pgTsRkh4E//DN5,iv:nHA+fHGV6pAh+khwaBQLHgqdSjHY7U8MCDTgFWcgzEg=,tag:1zyzN4/L9LWkBZ0kjusLhg==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age175l8mdpdfu7hq0aml3exs3gykarwjmuvr23sq56s3wjejnyk44wsp4mfzg + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZT3B5K3BETnlyK3JSQzV1 + aFc4RlZBdFBza1Y1cDR1dnJJZGdySjNEY25RCkdsYVltWGlRSlQwTnRYMnFoNHZW + OXZWOHFYRS9RbEtmVjczRUh5V0VBZ2cKLS0tICtvSlp6YkQ3S2s4bWNLMzhKeEd2 + K2h1TjV2aFdxdWhHZ1RqOXhTS0g3OUEKQdlDeBcuzhkCjh3ubLp8F7cn9dsqNEni + hYv08MMmO8xR1fTUvShloWYeg3/UnnjM/jihPKzvS9wIdv4fTY2ShA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-08-26T06:30:08Z" + mac: ENC[AES256_GCM,data:u1DrTgAtbzzIKjZwEdDYxtWSN3yFf2yCZ8u9QZZDglT7KwjNRdgckpeoNst5Qq0iGkzldKQmBeSKkOvSeMe1iFam9qKM3abcqDRbaw+cAr9XKaUgKaNKyVvaiHqNYoaWynn5GcxKfe6TrYAAxTN4kJAAk71sSzbTP0fTEO0tOKs=,iv:LiSIBo1qZ7/txIqsHEb9hnhXLZI15NOU/byLN4skL/o=,tag:oxwNBvRP77TTzwAbK8F9hA==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.9.4 diff --git a/apps/nextcloud-db.yaml b/apps/nextcloud-db.yaml new file mode 100644 index 0000000..edb8fb1 --- /dev/null +++ b/apps/nextcloud-db.yaml @@ -0,0 +1,74 @@ +--- +# Nextcloud's PostgreSQL — a CloudNativePG Cluster (CR of the CNPG CRDs from platform/cnpg), modeled +# on platform/keycloak/postgres-cluster.yaml. This is the first CNPG-DB in apps/ and establishes the +# reusable pattern for later DB-backed services (semaphore, unifi, ...). +# +# WHY A DEDICATED `nextcloud-db` NAMESPACE (not co-located in `nextcloud`): +# The app->db edge in catalog/nextcloud.yaml (`dependencies: to: nextcloud-db`) compiles to a +# CiliumNetworkPolicy whose egress selector pins BOTH the destination pod label +# (app.kubernetes.io/name=nextcloud-db) AND its namespace (k8s:io.kubernetes.pod.namespace= +# nextcloud-db) — cross-namespace Cilium selectors must name the namespace (catalog/render.py +# r_dependency_policies). So the DB MUST live in a namespace literally named `nextcloud-db` and its +# pods MUST carry app.kubernetes.io/name=nextcloud-db, injected below via inheritedMetadata. +# +# DEFAULT-DENY POSTURE — mirrors keycloak (platform/keycloak/namespace.yaml): this namespace is +# INTENTIONALLY not homelab.keithjackson.dev/app-namespace=true, so Kyverno does NOT generate the app +# default-deny baseline here and CNPG runs under Cilium's cluster-wide default-allow — the SAME posture +# the verified keycloak-db uses. The nextcloud SIDE keeps its default-deny; its rendered egress-allow +# opens ONLY nextcloud -> nextcloud-db:5432, and the DB side's default-allow accepts that reciprocal +# ingress. No app-namespace policy (auth-mode/host-namespace/secret-provenance) is weakened. +apiVersion: v1 +kind: Namespace +metadata: + name: nextcloud-db + labels: + # Match the Cilium egress selector's namespace pin; NOT an app-namespace (see header). + app.kubernetes.io/name: nextcloud-db +--- +# Owner credentials for the CNPG cluster. SOPS-encrypted (age); type basic-auth so CNPG's initdb +# consumes it as the `nextcloud` role's password instead of generating a random one — this lets the +# SAME password be mirrored into ns nextcloud (nextcloud-bootstrap/db-password) for the app's env, +# since a Pod cannot read a Secret in another namespace. Provided as apps/nextcloud-db-app.sops.yaml. +# +# CNPG Cluster (CR of postgresql.cnpg.io installed by platform/cnpg). +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: nextcloud-db + namespace: nextcloud-db +spec: + # Single Postgres instance on 3-replica Longhorn is acceptable at home scale (ADR-0014, same as + # keycloak-db). The storage is already 3-way replicated by Longhorn; raise to 3 for Postgres-level + # HA as a future hardening. + instances: 1 + imageName: ghcr.io/cloudnative-pg/postgresql:16.4 + # Stamp app.kubernetes.io/name=nextcloud-db onto every generated pod/service so the rendered + # nextcloud egress CiliumNetworkPolicy's toEndpoints selector matches the primary pod (R-NET-02). + inheritedMetadata: + labels: + app.kubernetes.io/name: nextcloud-db + storage: + storageClass: longhorn + size: 5Gi + bootstrap: + initdb: + database: nextcloud + owner: nextcloud + # Use the pre-provisioned owner secret (basic-auth) so its password is known + reusable by the + # app in the other namespace, instead of the random CNPG-generated one. + secret: + name: nextcloud-db-app + # Emit Postgres metrics so Prometheus scrapes DB health (R-OBS-01). The CNPG operator creates the + # PodMonitor at runtime (CRD present from Phase-4 monitoring). + monitoring: + enablePodMonitor: true + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 512Mi + # --- Off-cluster DR backup (R-STATE-03) — OPERATOR HAND-OFF (OQ-BKP, shared with Velero, Phase 4) --- + # When the off-cluster backup target is chosen, add a Barman object store + a ScheduledBackup here + # (same hand-off as keycloak-db). Until then the DB is protected by 3-replica Longhorn + (Phase-4) + # Velero PVC backups of the namespace; the SD declares backup.policy daily-offcluster as the intent. diff --git a/apps/nextcloud.yaml b/apps/nextcloud.yaml new file mode 100644 index 0000000..345e81d --- /dev/null +++ b/apps/nextcloud.yaml @@ -0,0 +1,264 @@ +--- +# Namespace (trust zone; default-deny + policies apply) +apiVersion: v1 +kind: Namespace +metadata: + name: nextcloud + labels: + homelab.keithjackson.dev/app-namespace: 'true' + gateway-access: shared + annotations: + homelab.keithjackson.dev/backup-policy: daily-offcluster +--- +# Default-deny baseline (R-NET-01): deny all, allow only DNS +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny + namespace: nextcloud + labels: + homelab.keithjackson.dev/baseline: default-deny +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress + ingress: [] + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 +--- +# PVC nextcloud-data (R-STATE) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: nextcloud-data + namespace: nextcloud + labels: + app.kubernetes.io/name: nextcloud +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 20Gi +--- +# Deployment (R-PLAT-02) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nextcloud + namespace: nextcloud + labels: + app.kubernetes.io/name: nextcloud +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: nextcloud + template: + metadata: + labels: + app.kubernetes.io/name: nextcloud + homelab.keithjackson.dev/egress-class: internet + homelab.keithjackson.dev/auth-mode: oidc + annotations: + homelab.keithjackson.dev/auth-mode: oidc + homelab.keithjackson.dev/auth-groups: platform-admins + spec: + containers: + - name: nextcloud + image: nextcloud:31-apache + ports: + - containerPort: 80 + env: + - name: POSTGRES_HOST + value: nextcloud-db-rw.nextcloud-db.svc.cluster.local + - name: POSTGRES_DB + value: nextcloud + - name: POSTGRES_USER + value: nextcloud + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: nextcloud-bootstrap + key: db-password + - name: NEXTCLOUD_ADMIN_USER + valueFrom: + secretKeyRef: + name: nextcloud-bootstrap + key: admin-username + - name: NEXTCLOUD_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: nextcloud-bootstrap + key: admin-password + - name: NEXTCLOUD_TRUSTED_DOMAINS + value: nextcloud.home.keithjackson.dev + - name: OVERWRITEHOST + value: nextcloud.home.keithjackson.dev + - name: OVERWRITEPROTOCOL + value: https + - name: OVERWRITECLIURL + value: https://nextcloud.home.keithjackson.dev + volumeMounts: + - name: data + mountPath: /var/www/html + volumes: + - name: data + persistentVolumeClaim: + claimName: nextcloud-data +--- +# Service +apiVersion: v1 +kind: Service +metadata: + name: nextcloud + namespace: nextcloud + labels: + app.kubernetes.io/name: nextcloud +spec: + selector: + app.kubernetes.io/name: nextcloud + ports: + - name: port-80 + port: 80 + targetPort: 80 +--- +# CiliumNetworkPolicy egress allow (R-NET-02): nextcloud-allow-nextcloud-db-5432 +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: nextcloud-allow-nextcloud-db-5432 + namespace: nextcloud + labels: + app.kubernetes.io/name: nextcloud +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/name: nextcloud + egress: + - toEndpoints: + - matchLabels: + app.kubernetes.io/name: nextcloud-db + k8s:io.kubernetes.pod.namespace: nextcloud-db + toPorts: + - ports: + - port: '5432' + protocol: TCP +--- +# CiliumNetworkPolicy gateway-ingress allow (R-NET-01/02): nextcloud-allow-gateway-ingress +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: nextcloud-allow-gateway-ingress + namespace: nextcloud + labels: + app.kubernetes.io/name: nextcloud +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/name: nextcloud + ingress: + - fromEntities: + - ingress + toPorts: + - ports: + - port: '80' + protocol: TCP +--- +# CiliumNetworkPolicy world egress allow (egress class governs the path; R-NET-05): nextcloud-allow-internet-egress +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: nextcloud-allow-internet-egress + namespace: nextcloud + labels: + app.kubernetes.io/name: nextcloud +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/name: nextcloud + egress: + - toEntities: + - world +--- +# HTTPRoute nextcloud-80 (R-TLS/R-DNS) +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: nextcloud-80 + namespace: nextcloud + labels: + app.kubernetes.io/name: nextcloud + annotations: + homelab.keithjackson.dev/auth-mode: oidc + homelab.keithjackson.dev/auth-groups: platform-admins +spec: + parentRefs: + - name: shared + namespace: gateway + sectionName: https + hostnames: + - nextcloud.home.keithjackson.dev + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: nextcloud + port: 80 +--- +# ServiceMonitor (R-OBS-01) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: nextcloud + namespace: nextcloud + labels: + release: kube-prometheus-stack + app.kubernetes.io/name: nextcloud +spec: + selector: + matchLabels: + app.kubernetes.io/name: nextcloud + namespaceSelector: + matchNames: + - nextcloud + endpoints: + - port: metrics + interval: 30s +--- +# PrometheusRule SLI alerts (R-OBS-01) +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: nextcloud-slis + namespace: nextcloud + labels: + release: kube-prometheus-stack + app.kubernetes.io/name: nextcloud +spec: + groups: + - name: nextcloud.slis + rules: + - alert: nextcloud-down + expr: up{job="nextcloud"} == 0 + for: 5m + labels: + severity: critical + service: nextcloud + annotations: + summary: nextcloud is down + description: 'nextcloud has been unreachable for 5m (SLI: up).' diff --git a/catalog/nextcloud.yaml b/catalog/nextcloud.yaml new file mode 100644 index 0000000..e60bcf8 --- /dev/null +++ b/catalog/nextcloud.yaml @@ -0,0 +1,98 @@ +# ServiceDefinition: nextcloud +# Source of truth: spec docs/13-service-catalog.md row 7; docs/15-identity-and-access.md; docs/04. +# public file-sync | egress internet | data on Longhorn (irreplaceable, DR-critical) | auth oidc. +# +# DR-critical (docs/13 "DR-critical data"): the user data lives on the 3-replica `longhorn` tier and +# is backed up off-cluster (backup.policy daily-offcluster; R-STATE-03). Nextcloud is stateless code +# over (a) its /var/www/html Longhorn PVC and (b) an external PostgreSQL — the first CNPG-DB app, +# establishing the reusable pattern (its DB is a CloudNativePG Cluster, apps/nextcloud-db.yaml). +# +# DB WIRING (schema >=1.2.0 env/envFrom): the app reaches its PostgreSQL cross-namespace at +# nextcloud-db-rw.nextcloud-db:5432 (the CNPG read-write Service). Its owner credentials are the +# SAME password held in two SOPS secrets — nextcloud-db-app (basic-auth, ns nextcloud-db; consumed by +# the CNPG Cluster's initdb) and nextcloud-bootstrap (ns nextcloud; consumed here as db-password). +# A Pod can only read Secrets in its OWN namespace, so the shared password is duplicated across the +# two encrypted files rather than replicated cross-namespace (documented in apps/nextcloud-db.yaml). +# Validates against schemas/servicedefinition.schema.json. +name: nextcloud +kind: container +source: + image: nextcloud:31-apache # apache variant serves on :80 (docs/13 row 7); pinned recent stable + # major (R-UPD-01); update.range governs advancement within the line +env: + # --- PostgreSQL connection (the CNPG-DB pattern). HOST/DB/USER are literals; PASSWORD is sourced + # from the in-namespace SOPS secret (the CNPG -app secret lives in ns nextcloud-db, unreachable + # by a Pod secretKeyRef, so the same value is mirrored into nextcloud-bootstrap here). --- + - name: POSTGRES_HOST + value: nextcloud-db-rw.nextcloud-db.svc.cluster.local + - name: POSTGRES_DB + value: nextcloud + - name: POSTGRES_USER + value: nextcloud + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: nextcloud-bootstrap + key: db-password + # --- Break-glass local admin (docs/15 availability caveat): created on first-boot install so the + # operator is never locked out by an IdP outage. Sourced from the SOPS nextcloud-bootstrap secret. --- + - name: NEXTCLOUD_ADMIN_USER + valueFrom: + secretKeyRef: + name: nextcloud-bootstrap + key: admin-username + - name: NEXTCLOUD_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: nextcloud-bootstrap + key: admin-password + # --- Public exposure behind the TLS-terminating shared Gateway (docs/08/09). TRUSTED_DOMAINS lets + # the vhost answer for the public FQDN; OVERWRITE* make Nextcloud emit https:// self-URLs even + # though it speaks plain HTTP behind the proxy. --- + - name: NEXTCLOUD_TRUSTED_DOMAINS + value: nextcloud.home.keithjackson.dev + - name: OVERWRITEHOST + value: nextcloud.home.keithjackson.dev + - name: OVERWRITEPROTOCOL + value: https + - name: OVERWRITECLIURL + value: https://nextcloud.home.keithjackson.dev +exposure: public # edge port-forward + public DNS (docs/09; R-DNS/R-NET) +egress: internet # normal WAN egress (docs/03; R-NET-05) — app store, integrity checks +storage: + - name: data + size: 20Gi + class: longhorn # ALL Nextcloud state (config/, data/, apps/, custom_apps/) on the + mountPath: /var/www/html # 3-replica tier; irreplaceable, DR-critical (docs/04; R-STATE-02). + # The whole html dir is persisted so config.php + user data survive a + # rebuild (the default /data/ would strand them on ephemeral rootfs). +dependencies: + - to: nextcloud-db # explicit allow: Nextcloud -> its PostgreSQL (docs/13 graph; R-NET-02). + port: 5432 # Renders the egress half; the DB namespace mirrors keycloak-db (no + protocol: tcp # default-deny), so Cilium's default-allow accepts the reciprocal ingress. +ingressRoutes: + - host: nextcloud.home.keithjackson.dev + port: 80 # apache HTTP; wildcard TLS terminates at the shared Gateway (docs/08/09) +auth: + mode: oidc # native Keycloak OIDC client (docs/13 SSO row, docs/15 "Native OIDC"; R-IDP-02) + groups: + - platform-admins # access scoped to an authorized group (R-IDP-06) + # NOTE (OIDC follow-up + break-glass): like Gitea (docs/15 "Availability caveat"), Nextcloud keeps a + # LOCAL admin as the break-glass path so an IdP outage never locks the operator out. At initial deploy + # it comes up INSTALLED on that local admin (env above). The Keycloak `nextcloud` realm client + its + # user_oidc app config is a DEFERRED follow-up: wiring it means editing the reconciled realm import in + # platform-config (a malformed edit fails the whole platform-config apply / can leave the realm import + # PENDING) AND post-install `occ` steps the image has no first-class env for — deferred rather than + # risking the running Keycloak realm / Flux source (WS-APPS step 3 escape hatch). This records the + # target integration; it embeds no secret. +update: + channel: stable # R-UPD-01 + range: ">=31.0.0 <32.0.0" # stay on the 31.x stable line + cadence: "0 4 * * *" + autoRollbackOnFailedHealth: true # R-UPD-03 +observability: + metrics: true + slis: + - up # file-sync down is a critical alert (docs/10; R-OBS-01) +backup: + policy: daily-offcluster # DR-critical: data backed up off-cluster (docs/13, docs/04; R-STATE-03)