From 933fea89d1f215c55237f0c7ea68a98d316a235f Mon Sep 17 00:00:00 2001 From: Andrew Stoltz Date: Thu, 4 Jun 2026 00:49:36 -0500 Subject: [PATCH] feat(auth): adopt oidc apps in gitops --- apps/fc-distribution/fc-distribution.yaml | 18 +- apps/fc-dns/fc-dns.yaml | 480 ++++++++++++++++++ apps/fc-dns/kustomization.yaml | 6 + apps/fc-media/fc-media.yaml | 295 +++++++++++ apps/fc-media/kustomization.yaml | 6 + apps/monitoring/noc-monitoring.yaml | 6 +- .../FleetManifestLintTests.cs | 154 +++++- 7 files changed, 955 insertions(+), 10 deletions(-) create mode 100644 apps/fc-dns/fc-dns.yaml create mode 100644 apps/fc-dns/kustomization.yaml create mode 100644 apps/fc-media/fc-media.yaml create mode 100644 apps/fc-media/kustomization.yaml diff --git a/apps/fc-distribution/fc-distribution.yaml b/apps/fc-distribution/fc-distribution.yaml index 29b75af..0e56681 100644 --- a/apps/fc-distribution/fc-distribution.yaml +++ b/apps/fc-distribution/fc-distribution.yaml @@ -74,6 +74,14 @@ metadata: spec: itemPath: "vaults/IAmWorkin/items/FlowerCore Edition Signing Key - edition:aistation-field" --- +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: distribution-oidc-client + namespace: fc-distribution +spec: + itemPath: "vaults/IAmWorkin/items/distribution-oidc-client" +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -130,13 +138,11 @@ spec: value: "Production" - name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT value: "false" - # Authentik/OIDC enforcement (flipped ON 2026-06-04, no-live-proof per operator; - # public read/entitlement + Method() allowlist stay open — OIDC gates admin only). - # Auth__Enabled reverted to false 2026-06-04: enabling it gated the - # /healthz readiness probe (probe->302->NotReady->endpoints drop->down). - # Re-enable once /healthz is AllowAnonymous (falcon OIDC lane). + # Authentik/OIDC enforcement. Public read/entitlement + the + # dist.flowercore.io Method() allowlist stay open; OIDC gates the + # operator/admin surface while /healthz remains anonymous. - name: FlowerCore__Auth__Enabled - value: "false" + value: "true" - name: FlowerCore__Auth__Oidc__Enabled value: "true" - name: FlowerCore__Auth__Oidc__Authority diff --git a/apps/fc-dns/fc-dns.yaml b/apps/fc-dns/fc-dns.yaml new file mode 100644 index 0000000..22c5192 --- /dev/null +++ b/apps/fc-dns/fc-dns.yaml @@ -0,0 +1,480 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: fc-dns + labels: + app.kubernetes.io/part-of: flowercore +--- +# 1Password-backed Secret for the pfSense admin password. +# The operator watches this CRD, resolves the vault item, and produces a +# K8s Secret of the same name with each 1P field as a key. The `password` +# field of the "pfSense Admin" item becomes Secret key `password`. +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: pfsense-admin + namespace: fc-dns +spec: + itemPath: "vaults/IAmWorkin/items/pfSense Admin" +--- +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: dns-oidc-client + namespace: fc-dns +spec: + itemPath: "vaults/IAmWorkin/items/dns-oidc-client" +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: dns-web-data + namespace: fc-dns +spec: + accessModes: [ReadWriteOnce] + storageClassName: longhorn + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: dns-web-config + namespace: fc-dns +data: + appsettings.Production.json: | + { + "FlowerCore": { + "Auth": { + "Enabled": true, + "Oidc": { + "Enabled": true, + "Audience": "dns", + "RequireHttpsMetadata": true + } + }, + "Database": { + "Provider": "Sqlite", + "ConnectionStrings": { + "Sqlite": "Data Source=/data/dns.db" + } + }, + "Tenant": { + "DefaultTenantId": "default", + "JwtClaimsEnabled": false, + "DefaultTenantHosts": [ + "dns.iamworkin.lan" + ] + }, + "Audit": { + "HashChain": { + "BridgeSensitivity": { + "Distribution": "Warn" + } + } + } + } + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dns-web + namespace: fc-dns + labels: + app.kubernetes.io/name: dns-web + app.kubernetes.io/managed-by: flowercore +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: dns-web + template: + metadata: + labels: + app.kubernetes.io/name: dns-web + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "5320" + prometheus.io/path: "/metrics/prometheus" + spec: + serviceAccountName: dns-web + securityContext: + runAsNonRoot: true + runAsUser: 1654 + runAsGroup: 1654 + fsGroup: 1654 + containers: + - name: dns-web + image: localhost/fc-dns-web:v20260604-oidc-proper + imagePullPolicy: Never + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + ports: + - containerPort: 5320 + env: + # pfSense admin password resolved by the 1Password operator. + # `FallbackPassword` is the Slice A seam exposed by + # OptionsFallbackPasswordResolver; Slice B will replace it with + # a pull-at-runtime 1P Connect resolver once Shared.Vault ships. + - name: FlowerCore__Dns__Providers__PfSenseUnbound__FallbackPassword + valueFrom: + secretKeyRef: + name: pfsense-admin + key: password + - name: FlowerCore__Auth__Oidc__Authority + valueFrom: + secretKeyRef: + name: dns-oidc-client + key: issuer_url + optional: true + - name: FlowerCore__Auth__Oidc__ClientId + valueFrom: + secretKeyRef: + name: dns-oidc-client + key: client_id + optional: true + - name: FlowerCore__Auth__Oidc__ClientSecret + valueFrom: + secretKeyRef: + name: dns-oidc-client + key: client_secret + optional: true + - name: FlowerCore__Auth__Enabled + value: "true" + - name: FlowerCore__Auth__Oidc__Enabled + value: "true" + - name: FlowerCore__Auth__Oidc__Audience + value: "dns" + volumeMounts: + - name: data + mountPath: /data + - name: tmp + mountPath: /tmp + - name: logs + mountPath: /app/logs + - name: config + mountPath: /app/appsettings.Production.json + subPath: appsettings.Production.json + readOnly: true + resources: + requests: + cpu: 50m + memory: 96Mi + limits: + cpu: 300m + memory: 384Mi + readinessProbe: + httpGet: + path: /healthz + port: 5320 + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /healthz + port: 5320 + initialDelaySeconds: 20 + periodSeconds: 30 + volumes: + - name: data + persistentVolumeClaim: + claimName: dns-web-data + - name: tmp + emptyDir: {} + - name: logs + emptyDir: {} + - name: config + configMap: + name: dns-web-config +--- +apiVersion: v1 +kind: Service +metadata: + name: dns-web + namespace: fc-dns +spec: + selector: + app.kubernetes.io/name: dns-web + ports: + - port: 5320 + targetPort: 5320 + type: ClusterIP +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dns-web + namespace: fc-dns +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dns-web +rules: + - apiGroups: [""] + resources: ["namespaces", "pods", "services", "secrets", "configmaps"] + verbs: ["get", "list", "watch"] + - apiGroups: ["cert-manager.io"] + resources: ["certificates"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: dns-web +subjects: + - kind: ServiceAccount + name: dns-web + namespace: fc-dns +roleRef: + kind: ClusterRole + name: dns-web + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: dns-web-cert + namespace: fc-dns +spec: + secretName: dns-web-tls + issuerRef: + name: step-ca-dns01 + kind: ClusterIssuer + dnsNames: + - dns.iamworkin.lan + duration: 720h + renewBefore: 240h +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: dns-web + namespace: fc-dns +spec: + entryPoints: [websecure] + routes: + - match: Host(`dns.iamworkin.lan`) + kind: Rule + services: + - name: dns-web + port: 5320 + tls: + secretName: dns-web-tls +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dns-acme-webhook + namespace: fc-dns +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dns-acme-webhook + namespace: fc-dns + labels: + app.kubernetes.io/name: dns-acme-webhook + app.kubernetes.io/managed-by: flowercore +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: dns-acme-webhook + template: + metadata: + labels: + app.kubernetes.io/name: dns-acme-webhook + spec: + serviceAccountName: dns-acme-webhook + securityContext: + runAsNonRoot: true + runAsUser: 1654 + runAsGroup: 1654 + fsGroup: 1654 + containers: + - name: dns-acme-webhook + image: localhost/fc-dns-acme-webhook:v202604290845 + imagePullPolicy: Never + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + ports: + - containerPort: 9443 + name: https + env: + - name: ASPNETCORE_URLS + value: https://+:9443 + - name: Kestrel__Certificates__Default__Path + value: /tls/tls.crt + - name: Kestrel__Certificates__Default__KeyPath + value: /tls/tls.key + - name: FlowerCore__Dns__AcmeWebhook__ServiceBaseUrl + value: http://dns-web:5320 + - name: FlowerCore__Dns__AcmeWebhook__GroupName + value: acme.flowercore.io + - name: FlowerCore__Dns__AcmeWebhook__SolverName + value: flowercore-dns + - name: FlowerCore__Dns__AcmeWebhook__Version + value: v1alpha1 + volumeMounts: + - name: tls + mountPath: /tls + readOnly: true + - name: tmp + mountPath: /tmp + - name: logs + mountPath: /app/logs + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 200m + memory: 256Mi + readinessProbe: + httpGet: + scheme: HTTPS + path: /readyz + port: https + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + httpGet: + scheme: HTTPS + path: /healthz + port: https + initialDelaySeconds: 10 + periodSeconds: 20 + timeoutSeconds: 5 + volumes: + - name: tls + secret: + secretName: dns-acme-webhook-tls + - name: tmp + emptyDir: {} + - name: logs + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: dns-acme-webhook + namespace: fc-dns +spec: + selector: + app.kubernetes.io/name: dns-acme-webhook + ports: + - port: 443 + targetPort: https + name: https + type: ClusterIP +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: dns-acme-webhook-selfsigned + namespace: fc-dns +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: dns-acme-webhook-ca + namespace: fc-dns +spec: + secretName: dns-acme-webhook-ca + duration: 43800h + issuerRef: + name: dns-acme-webhook-selfsigned + commonName: ca.dns-acme-webhook.fc-dns + isCA: true +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: dns-acme-webhook-ca-issuer + namespace: fc-dns +spec: + ca: + secretName: dns-acme-webhook-ca +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: dns-acme-webhook-serving-cert + namespace: fc-dns +spec: + secretName: dns-acme-webhook-tls + duration: 8760h + issuerRef: + name: dns-acme-webhook-ca-issuer + dnsNames: + - dns-acme-webhook + - dns-acme-webhook.fc-dns + - dns-acme-webhook.fc-dns.svc +--- +apiVersion: apiregistration.k8s.io/v1 +kind: APIService +metadata: + name: v1alpha1.acme.flowercore.io + annotations: + cert-manager.io/inject-ca-from: fc-dns/dns-acme-webhook-serving-cert +spec: + group: acme.flowercore.io + groupPriorityMinimum: 1000 + service: + name: dns-acme-webhook + namespace: fc-dns + version: v1alpha1 + versionPriority: 15 +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dns-acme-webhook-solver +rules: + - apiGroups: ["acme.flowercore.io"] + resources: ["flowercore-dns"] + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: dns-acme-webhook-solver +subjects: + - kind: ServiceAccount + name: cert-manager + namespace: cert-manager +roleRef: + kind: ClusterRole + name: dns-acme-webhook-solver + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: step-ca-dns01 +spec: + acme: + caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJ4RENDQVdxZ0F3SUJBZ0lSQVBZMzU3RzZvdzZ6TUFMNSs0YlMya2t3Q2dZSUtvWkl6ajBFQXdJd1FERWEKTUJnR0ExVUVDaE1SU1VGdFYyOXlhMmx1SUVGRFRVVWdRMEV4SWpBZ0JnTlZCQU1UR1VsQmJWZHZjbXRwYmlCQgpRMDFGSUVOQklGSnZiM1FnUTBFd0hoY05Nall3TXpBNE1UZ3dOekV4V2hjTk16WXdNekExTVRnd056RXhXakJBCk1Sb3dHQVlEVlFRS0V4RkpRVzFYYjNKcmFXNGdRVU5OUlNCRFFURWlNQ0FHQTFVRUF4TVpTVUZ0VjI5eWEybHUKSUVGRFRVVWdRMEVnVW05dmRDQkRRVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSjJuMDRYMQpKWm81WmRxL2kxSWR2OCtmcXdaeUF6Qmg3d2hicWowU1dzSkw4VVdSYWJDTXFZQ3M3K2RYTzB4UlN6cWt3RkRMCngrdm9vT2FpOFJnUk5oYWpSVEJETUE0R0ExVWREd0VCL3dRRUF3SUJCakFTQmdOVkhSTUJBZjhFQ0RBR0FRSC8KQWdFQk1CMEdBMVVkRGdRV0JCUm51UFBRUjZpTS9INnZPbHVpVTNTeWdheXo4akFLQmdncWhrak9QUVFEQWdOSQpBREJGQWlFQXJRSzlkWVBHbUFac2RZbmp6aXVGVlZFNU5LWlVjY2VZdkdmR0MrdExYVXNDSUF1ZEYyekpyQ1JxCjNtSzUwWlpFVC9md1RrSndpRUY0ODI0bWpQOHAxQ0tNCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + privateKeySecretRef: + name: step-ca-dns01-account-key + server: https://10.0.56.10:9443/acme/acme/directory + solvers: + - dns01: + webhook: + groupName: acme.flowercore.io + solverName: flowercore-dns diff --git a/apps/fc-dns/kustomization.yaml b/apps/fc-dns/kustomization.yaml new file mode 100644 index 0000000..631cea5 --- /dev/null +++ b/apps/fc-dns/kustomization.yaml @@ -0,0 +1,6 @@ +# ArgoCD's bluejay-infra ApplicationSet discovers apps/* directories on main. +# The kustomization is included for local previews and single-app validation. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - fc-dns.yaml diff --git a/apps/fc-media/fc-media.yaml b/apps/fc-media/fc-media.yaml new file mode 100644 index 0000000..0ad043e --- /dev/null +++ b/apps/fc-media/fc-media.yaml @@ -0,0 +1,295 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: fc-media + labels: + app.kubernetes.io/name: fc-media + app.kubernetes.io/part-of: flowercore +--- +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: media-oidc-client + namespace: fc-media + labels: + app.kubernetes.io/name: fc-media-web + app.kubernetes.io/part-of: flowercore +spec: + itemPath: "vaults/IAmWorkin/items/media-oidc-client" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: fc-media-config + namespace: fc-media + labels: + app.kubernetes.io/name: fc-media-web + app.kubernetes.io/part-of: flowercore +data: + appsettings.Production.json: | + { + "DatabaseProvider": "Sqlite", + "ConnectionStrings": { + "Sqlite": "Data Source=/data/media.db" + }, + "FlowerCore": { + "Auth": { + "Enabled": true, + "Oidc": { + "Authority": "https://id.iamworkin.lan/application/o/media/", + "ClientId": "media", + "ClientSecret": "", + "Audience": "media", + "RequireHttpsMetadata": true + } + }, + "Tenant": { + "JwtClaimsEnabled": false, + "DefaultTenantHosts": [ "media.iamworkin.lan" ] + } + }, + "Media": { + "LibraryRoot": "/media/library", + "Sources": [ + { + "Name": "BlueJayNAS Video", + "Driver": "Nfs", + "MountedPath": "/media/library", + "RemotePath": "nfs://10.0.58.3/volume1/video", + "IsEnabled": true, + "IsDefault": true, + "Notes": "Synology NFS media share mounted read-only inside the cluster." + } + ], + "GeneratedRoot": "/data/generated", + "TranscodeRoot": "/data/transcodes", + "InboxPath": "/media/inbox", + "InboxScanIntervalMinutes": 5, + "ScanOnStartup": false, + "ComputeChecksums": false, + "FfmpegCommand": "ffmpeg", + "FfprobeCommand": "ffprobe", + "Hls": { + "MaxConcurrentJobs": 1 + }, + "DefaultViewerName": "BlueJay", + "Dlna": { + "IsEnabled": true, + "MulticastAddress": "239.255.255.250", + "Port": 1900, + "DiscoveryTimeoutSeconds": 2, + "DescriptionFetchTimeoutSeconds": 2, + "MaxResponsesPerSearchTarget": 32, + "SearchTargets": [ + "urn:schemas-upnp-org:device:MediaRenderer:1", + "urn:schemas-upnp-org:device:MediaServer:1" + ] + } + } + } +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: fc-media-data + namespace: fc-media + labels: + app.kubernetes.io/name: fc-media-web + app.kubernetes.io/part-of: flowercore +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi + storageClassName: longhorn +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fc-media-web + namespace: fc-media + labels: + app: fc-media-web + app.kubernetes.io/name: fc-media-web + app.kubernetes.io/part-of: flowercore +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: fc-media-web + template: + metadata: + labels: + app: fc-media-web + app.kubernetes.io/name: fc-media-web + app.kubernetes.io/part-of: flowercore + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "5200" + prometheus.io/path: "/metrics" + spec: + nodeSelector: + kubernetes.io/hostname: rke2-server + containers: + - name: fc-media-web + image: localhost/fc-media-web:v20260604-oidc-proper + imagePullPolicy: Never + ports: + - containerPort: 5200 + name: http + env: + - name: ASPNETCORE_ENVIRONMENT + value: Production + - name: ASPNETCORE_URLS + value: http://+:5200 + - name: FlowerCore__Auth__Enabled + value: "true" + - name: FlowerCore__Auth__Oidc__Enabled + value: "true" + - name: FlowerCore__Auth__Oidc__Audience + value: "media" + - name: FlowerCore__Auth__Oidc__ClientId + valueFrom: + secretKeyRef: + name: media-oidc-client + key: client_id + optional: true + - name: FlowerCore__Auth__Oidc__ClientSecret + valueFrom: + secretKeyRef: + name: media-oidc-client + key: client_secret + optional: true + - name: FlowerCore__Auth__Oidc__Authority + valueFrom: + secretKeyRef: + name: media-oidc-client + key: issuer_url + optional: true + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: "4" + memory: 4Gi + volumeMounts: + - name: config + mountPath: /app/appsettings.Production.json + subPath: appsettings.Production.json + readOnly: true + - name: data + mountPath: /data + - name: transcodes + mountPath: /data/transcodes + - name: media-library + mountPath: /media/library + readOnly: true + - name: media-inbox + mountPath: /media/inbox + startupProbe: + httpGet: + path: /healthz + port: 5200 + httpHeaders: + - name: X-Forwarded-Proto + value: https + failureThreshold: 18 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /healthz + port: 5200 + httpHeaders: + - name: X-Forwarded-Proto + value: https + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /healthz + port: 5200 + httpHeaders: + - name: X-Forwarded-Proto + value: https + initialDelaySeconds: 30 + periodSeconds: 30 + volumes: + - name: config + configMap: + name: fc-media-config + - name: data + persistentVolumeClaim: + claimName: fc-media-data + - name: transcodes + nfs: + server: 10.0.58.3 + path: /volume1/kubernetes/fc-media-transcodes + - name: media-inbox + nfs: + server: 10.0.58.3 + path: /volume1/kubernetes/fc-media-inbox + - name: media-library + nfs: + server: 10.0.58.3 + path: /volume1/video + readOnly: true +--- +apiVersion: v1 +kind: Service +metadata: + name: fc-media-web + namespace: fc-media + labels: + app: fc-media-web + app.kubernetes.io/name: fc-media-web + app.kubernetes.io/part-of: flowercore +spec: + type: ClusterIP + selector: + app: fc-media-web + ports: + - port: 5200 + targetPort: 5200 + protocol: TCP + name: http +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: fc-media-tls + namespace: fc-media + labels: + app.kubernetes.io/name: fc-media-web + app.kubernetes.io/part-of: flowercore +spec: + secretName: fc-media-tls + issuerRef: + name: step-ca-acme + kind: ClusterIssuer + dnsNames: + - media.iamworkin.lan +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: fc-media-web + namespace: fc-media + labels: + app.kubernetes.io/name: fc-media-web + app.kubernetes.io/part-of: flowercore +spec: + entryPoints: + - websecure + routes: + - match: Host(`media.iamworkin.lan`) + kind: Rule + services: + - name: fc-media-web + port: 5200 + tls: + secretName: fc-media-tls diff --git a/apps/fc-media/kustomization.yaml b/apps/fc-media/kustomization.yaml new file mode 100644 index 0000000..d3ddd61 --- /dev/null +++ b/apps/fc-media/kustomization.yaml @@ -0,0 +1,6 @@ +# ArgoCD's bluejay-infra ApplicationSet discovers apps/* directories on main. +# The kustomization is included for local previews and single-app validation. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - fc-media.yaml diff --git a/apps/monitoring/noc-monitoring.yaml b/apps/monitoring/noc-monitoring.yaml index 08441bb..9e4668e 100644 --- a/apps/monitoring/noc-monitoring.yaml +++ b/apps/monitoring/noc-monitoring.yaml @@ -481,15 +481,15 @@ data: - "https://intranet.iamworkin.lan/" - "https://signage.iamworkin.lan/healthz" # root 401 auth-gated 2026-06-01; /healthz anon 200 - "https://kiosk.iamworkin.lan/" - - "https://media.iamworkin.lan/" + - "https://media.iamworkin.lan/healthz" # root auth-gated by OIDC; /healthz anon 200 - "https://mysql.iamworkin.lan/healthz" # root 401 auth-gated 2026-06-01; /healthz anon 200 - "https://php.iamworkin.lan/healthz" # root 401 auth-gated 2026-06-01; /healthz anon 200 - "https://zabbix.iamworkin.lan/" - "https://desktop.iamworkin.lan/" - "https://print.iamworkin.lan/" - - "https://dns.iamworkin.lan/" + - "https://dns.iamworkin.lan/healthz" # root auth-gated by OIDC; /healthz anon 200 - "https://chat.iamworkin.lan/" - - "https://dist.iamworkin.lan/" + - "https://dist.iamworkin.lan/healthz" # root/admin auth-gated by OIDC; /healthz anon 200 - "https://dms.iamworkin.lan/" - "https://menuboard.iamworkin.lan/" - "https://messageboard.iamworkin.lan/" diff --git a/tests/bluejay-infra-lint/FleetManifestLintTests.cs b/tests/bluejay-infra-lint/FleetManifestLintTests.cs index dfdc671..2448cf4 100644 --- a/tests/bluejay-infra-lint/FleetManifestLintTests.cs +++ b/tests/bluejay-infra-lint/FleetManifestLintTests.cs @@ -15,7 +15,6 @@ public sealed class FleetManifestLintTests { "brochure.flowercore.io", "dist.flowercore.io", - "dns.iamworkin.lan", }; // Public hosts that allow a tightly bounded write surface in addition to @@ -706,6 +705,140 @@ public sealed class FleetManifestLintTests application.Scalar("spec", "destination", "namespace").Should().Be("fc-devicemgmt"); } + [Fact] + public void OidcFlipServices_AreGitOpsManagedWithHealthzProbes() + { + var deployments = new[] + { + (App: "fc-dns", Name: "dns-web", Slug: "dns", Secret: "dns-oidc-client"), + (App: "fc-media", Name: "fc-media-web", Slug: "media", Secret: "media-oidc-client"), + (App: "fc-distribution", Name: "fc-distribution", Slug: "distribution", Secret: "distribution-oidc-client"), + }; + + foreach (var expected in deployments) + { + var deployment = AppDocuments(expected.App) + .Single(document => document.Kind == "Deployment" && document.Name == expected.Name); + var container = deployment.MainContainerMappings().Should().ContainSingle().Subject; + + EnvValue(container, "FlowerCore__Auth__Enabled").Should().Be("true"); + EnvValue(container, "FlowerCore__Auth__Oidc__Enabled").Should().Be("true"); + (EnvValue(container, "FlowerCore__Auth__Oidc__Audience") ?? EnvValue(container, "FlowerCore__Auth__Oidc__ClientId")) + .Should() + .Be(expected.Slug); + EnvSecretName(container, "FlowerCore__Auth__Oidc__ClientSecret").Should().Be(expected.Secret); + EnvSecretOptional(container, "FlowerCore__Auth__Oidc__ClientSecret").Should().Be("true"); + + ProbePath(container, "readinessProbe").Should().Be("/healthz"); + if (ProbePath(container, "startupProbe") is { } startupProbePath) + { + startupProbePath.Should().Be("/healthz"); + } + + if (ProbePath(container, "livenessProbe") is { } livenessProbePath) + { + livenessProbePath.Should().Be("/healthz"); + } + } + } + + [Fact] + public void OidcFlipServices_UseOnePasswordItemClientSecrets() + { + var expectedItems = new Dictionary(StringComparer.Ordinal) + { + ["fc-dns"] = ("dns-oidc-client", "vaults/IAmWorkin/items/dns-oidc-client"), + ["fc-media"] = ("media-oidc-client", "vaults/IAmWorkin/items/media-oidc-client"), + ["fc-distribution"] = ("distribution-oidc-client", "vaults/IAmWorkin/items/distribution-oidc-client"), + }; + + foreach (var expected in expectedItems) + { + var item = AppDocuments(expected.Key) + .Single(document => document.Kind == "OnePasswordItem" && document.Name == expected.Value.Name); + + item.Scalar("spec", "itemPath").Should().Be(expected.Value.ItemPath); + } + } + + [Fact] + public void DnsAndMediaGitOpsAdoption_PreservesLiveStorageAndImageShape() + { + var dnsDeployment = AppDocuments("fc-dns") + .Single(document => document.Kind == "Deployment" && document.Name == "dns-web"); + var dnsContainer = dnsDeployment.MainContainerMappings().Should().ContainSingle().Subject; + var dnsPvc = AppDocuments("fc-dns") + .Single(document => document.Kind == "PersistentVolumeClaim" && document.Name == "dns-web-data"); + + ManifestNodeExtensions.Scalar(dnsContainer, "image").Should().Be("localhost/fc-dns-web:v20260604-oidc-proper"); + dnsPvc.Scalar("spec", "storageClassName").Should().Be("longhorn"); + dnsPvc.Scalar("spec", "resources", "requests", "storage").Should().Be("1Gi"); + + var mediaDeployment = AppDocuments("fc-media") + .Single(document => document.Kind == "Deployment" && document.Name == "fc-media-web"); + var mediaContainer = mediaDeployment.MainContainerMappings().Should().ContainSingle().Subject; + var mediaPvc = AppDocuments("fc-media") + .Single(document => document.Kind == "PersistentVolumeClaim" && document.Name == "fc-media-data"); + + ManifestNodeExtensions.Scalar(mediaContainer, "image").Should().Be("localhost/fc-media-web:v20260604-oidc-proper"); + mediaPvc.Scalar("spec", "storageClassName").Should().Be("longhorn"); + mediaPvc.Scalar("spec", "resources", "requests", "storage").Should().Be("20Gi"); + + mediaDeployment.AllScalars().Should().Contain(new[] + { + "/volume1/kubernetes/fc-media-transcodes", + "/volume1/kubernetes/fc-media-inbox", + "/volume1/video", + }); + } + + [Fact] + public void MonitoringProbes_UseHealthzForOidcGatedHosts() + { + var monitoring = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "apps", "monitoring", "noc-monitoring.yaml")); + + monitoring.Should().Contain("\"https://dns.iamworkin.lan/healthz\""); + monitoring.Should().Contain("\"https://dist.iamworkin.lan/healthz\""); + monitoring.Should().Contain("\"https://media.iamworkin.lan/healthz\""); + monitoring.Should().NotContain("\"https://dns.iamworkin.lan/\""); + monitoring.Should().NotContain("\"https://dist.iamworkin.lan/\""); + monitoring.Should().NotContain("\"https://media.iamworkin.lan/\""); + } + + [Fact] + public void DistributionPublicIngress_KeepsGetHeadMethodAllowlist() + { + var publicIngress = AppDocuments("fc-distribution") + .Single(document => document.Kind == "IngressRoute" && document.Name == "fc-distribution-public"); + var route = publicIngress.MappingSequence("spec", "routes").Should().ContainSingle().Subject; + var match = ManifestNodeExtensions.Scalar(route, "match"); + + match.Should().Contain("Host(`dist.flowercore.io`)"); + match.Should().Contain("Method(`GET`)"); + match.Should().Contain("Method(`HEAD`)"); + match.Should().NotContain("Method(`POST`)"); + } + + [Fact] + public void DnsAndMediaIngressRoutes_MatchLiveInternalHosts() + { + var dnsRoute = AppDocuments("fc-dns") + .Single(document => document.Kind == "IngressRoute" && document.Name == "dns-web") + .MappingSequence("spec", "routes") + .Should() + .ContainSingle() + .Subject; + var mediaRoute = AppDocuments("fc-media") + .Single(document => document.Kind == "IngressRoute" && document.Name == "fc-media-web") + .MappingSequence("spec", "routes") + .Should() + .ContainSingle() + .Subject; + + ManifestNodeExtensions.Scalar(dnsRoute, "match").Should().Be("Host(`dns.iamworkin.lan`)"); + ManifestNodeExtensions.Scalar(mediaRoute, "match").Should().Be("Host(`media.iamworkin.lan`)"); + } + private static IEnumerable ProbeViolations( ManifestDocument document, YamlMappingNode container, @@ -762,6 +895,25 @@ public sealed class FleetManifestLintTests : null; } + private static string? EnvSecretOptional(YamlMappingNode container, string name) + { + return EnvMapping(container, name) is { } env + ? ManifestNodeExtensions.Scalar(env, "valueFrom", "secretKeyRef", "optional") + : null; + } + + private static string? ProbePath(YamlMappingNode container, string probeKey) + { + return ManifestNodeExtensions.Scalar(container, probeKey, "httpGet", "path"); + } + + private static IReadOnlyList AppDocuments(string app) + { + return Inventory.Documents + .Where(document => document.RelativePath.StartsWith($"{app}/", StringComparison.Ordinal)) + .ToList(); + } + private static YamlMappingNode? EnvMapping(YamlMappingNode container, string name) { return ManifestNodeExtensions.MappingSequence(container, "env") -- 2.49.1