# FlowerCore.Distribution — edition manifest publisher + content-addressed blob store. # Phase 1 of the USB provisioning architecture: signed edition manifests # (ECDSA P-256 over canonical JSON) published per edition, plus a SHA-256 # content-addressed blob store that USB builders pull from. # # Architecture: FlowerCore.Notes/docs/infrastructure/usb-provisioning-architecture.md # Repo: FlowerCore.Distribution/{README.md,CLAUDE.md} # Shared lib: FlowerCore.Common -> FlowerCore.Shared.Distribution # (manifest schema, canonical JSON, ECDSA P-256 sign/verify) # # Deployment order (see bluejay-infra/README.md and apps/fc-distribution/README.md): # 1. pfSense Unbound DNS override for dist.iamworkin.lan -> 10.0.56.200 # (DONE 2026-04-23 — verify with `python bluejay-infra/scripts/check-pfsense-dns.py`). # 2. 1Password items must exist in vault `IAmWorkin`: # - `FlowerCore Code Signing CA` (informational) # - `FlowerCore Edition Signing Key - edition:kiosk-standard` (3hf33egdvnni6jyuws3r737mqe) # - `FlowerCore Edition Signing Key - edition:aistation-field` (ccxrtsan5samfq4pfuczymacrq) # Each edition item is expected to publish three field labels: # certificate.pem, private-key.pem, chain.pem # 3. Synology NFS export `/volume1/kubernetes` is currently restricted to # rke2-server (10.0.56.11). Pod is pinned via nodeSelector below. The # app writes to subPaths `distribution/data` and `distribution/blobs`. # 4. Build + import image: localhost/fc-distribution:v # Import to rke2-server via `ctr images import` (NFS-pinned, no need # for the agents until ACL is widened — see guacamole pattern). # 5. Bump the image tag below and git push; ArgoCD ApplicationSet picks up # within ~3 minutes and creates `infra-fc-distribution`. # # NOTE on the root trust anchor: # The verifier needs an embedded root CA (`IAmWorkin ACME CA Root CA`). # That root is shipped INSIDE the published image (Phase 2 build step # bakes it into the bundle), NOT mounted from a Secret here. The # `codesigning-root-cert` OnePasswordItem below is informational only — # it gives operators a quick handle to the CA item from the cluster. --- apiVersion: v1 kind: Namespace metadata: name: fc-distribution labels: app.kubernetes.io/part-of: flowercore --- # Informational handle to the FlowerCore Code Signing CA item in 1Password. # Not consumed by the pod at runtime — the root trust anchor is baked into # the published image. Operators can `kubectl -n fc-distribution get secret # codesigning-root-cert` to discover the CA item URL/admin handle. apiVersion: onepassword.com/v1 kind: OnePasswordItem metadata: name: codesigning-root-cert namespace: fc-distribution spec: itemPath: "vaults/IAmWorkin/items/FlowerCore Code Signing CA" --- # Edition signing key + leaf cert + chain for edition:kiosk-standard. # 1Password item id: 3hf33egdvnni6jyuws3r737mqe # Operator syncs each field to a Secret key of the same name. Mounted # read-only at /signing/kiosk-standard inside the pod. apiVersion: onepassword.com/v1 kind: OnePasswordItem metadata: name: edition-kiosk-standard namespace: fc-distribution spec: itemPath: "vaults/IAmWorkin/items/FlowerCore Edition Signing Key - edition:kiosk-standard" --- # Edition signing key + leaf cert + chain for edition:aistation-field. # 1Password item id: ccxrtsan5samfq4pfuczymacrq apiVersion: onepassword.com/v1 kind: OnePasswordItem metadata: name: edition-aistation-field namespace: fc-distribution spec: itemPath: "vaults/IAmWorkin/items/FlowerCore Edition Signing Key - edition:aistation-field" --- apiVersion: apps/v1 kind: Deployment metadata: name: fc-distribution namespace: fc-distribution labels: app.kubernetes.io/name: fc-distribution app.kubernetes.io/part-of: flowercore spec: replicas: 1 revisionHistoryLimit: 3 strategy: # NFS-backed SQLite + blob store on a single node. Recreate avoids any # multi-attach overlap on the same NFS subPath during rollout. type: Recreate selector: matchLabels: app.kubernetes.io/name: fc-distribution template: metadata: labels: app.kubernetes.io/name: fc-distribution app.kubernetes.io/part-of: flowercore annotations: prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/metrics" spec: # Synology NFS export `/volume1/kubernetes` ACL only allows rke2-server # (10.0.56.11) right now. Until the ACL is widened in DSM (admin only), # this Pod must run on rke2-server or NFS mounts will be access-denied. nodeSelector: kubernetes.io/hostname: rke2-server securityContext: runAsNonRoot: true fsGroup: 1654 fsGroupChangePolicy: OnRootMismatch containers: - name: web # Placeholder tag — bump to the image you built + imported to # rke2-server before applying. Build with: # dotnet.exe publish -c Release -o deploy/app \ # src/FlowerCore.Distribution.Web/FlowerCore.Distribution.Web.csproj # podman build -t localhost/fc-distribution:v -f deploy/Dockerfile.deploy deploy image: localhost/fc-distribution:v202604232000 imagePullPolicy: Never ports: - containerPort: 8080 name: http env: - name: ASPNETCORE_URLS value: "http://+:8080" - name: ASPNETCORE_ENVIRONMENT value: "Production" - name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT value: "false" # SQLite connection (catalog + data-protection keys via FlowerCoreDbContext). # Read by Data/DatabaseProviderExtensions.cs in precedence order; Sqlite key wins. - name: FlowerCore__Database__Provider value: "Sqlite" - name: FlowerCore__Database__ConnectionStrings__Sqlite value: "Data Source=/data/distribution.db" # Content-addressed blob root (SHA-256 sharded on disk). # Bound by Services/NfsPvcBlobProvider.cs under FlowerCore:Distribution:Blobs. - name: FlowerCore__Distribution__Blobs__Root value: "/blobs" # Per-edition signing material — paths into the read-only # secret mounts below. Field labels in 1Password (and therefore # Secret key names) are: certificate.pem, private-key.pem, chain.pem - name: FlowerCore__Distribution__Signing__EditionCerts__kiosk-standard__CertPath value: "/signing/kiosk-standard/chain.pem" - name: FlowerCore__Distribution__Signing__EditionCerts__kiosk-standard__KeyPath value: "/signing/kiosk-standard/private-key.pem" - name: FlowerCore__Distribution__Signing__EditionCerts__aistation-field__CertPath value: "/signing/aistation-field/chain.pem" - name: FlowerCore__Distribution__Signing__EditionCerts__aistation-field__KeyPath value: "/signing/aistation-field/private-key.pem" resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 512Mi # /healthz is exposed by the scaffold (StartupGateMiddleware-aware). # Liveness uses tcpSocket as a cheap fallback in case a future # middleware change accidentally gates /healthz behind auth # (memory: feedback_k8s_probes_behind_auth_middleware). startupProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 30 readinessProbe: httpGet: path: /healthz port: 8080 periodSeconds: 10 failureThreshold: 3 livenessProbe: tcpSocket: port: 8080 initialDelaySeconds: 30 periodSeconds: 30 failureThreshold: 3 securityContext: runAsNonRoot: true runAsUser: 1654 runAsGroup: 1654 allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL volumeMounts: - name: sqlite mountPath: /data subPath: distribution/data - name: blobs mountPath: /blobs subPath: distribution/blobs - name: tmp mountPath: /tmp - name: logs mountPath: /app/logs - name: kiosk-standard mountPath: /signing/kiosk-standard readOnly: true - name: aistation-field mountPath: /signing/aistation-field readOnly: true volumes: # Synology NFS at /volume1/kubernetes — same export pattern as # apps/guacamole/guacamole.yaml (recordings volume). Pinned by # ACL to rke2-server. Never mount the subpath as nfs.path — # always mount the export root and use volumeMount.subPath. - name: sqlite nfs: server: 10.0.58.3 path: /volume1/kubernetes - name: blobs nfs: server: 10.0.58.3 path: /volume1/kubernetes - name: tmp emptyDir: {} - name: logs emptyDir: {} - name: kiosk-standard secret: secretName: edition-kiosk-standard defaultMode: 0400 - name: aistation-field secret: secretName: edition-aistation-field defaultMode: 0400 --- apiVersion: v1 kind: Service metadata: name: fc-distribution namespace: fc-distribution labels: app.kubernetes.io/name: fc-distribution app.kubernetes.io/part-of: flowercore spec: type: ClusterIP selector: app.kubernetes.io/name: fc-distribution ports: - name: http port: 80 targetPort: 8080 --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: fc-distribution-tls namespace: fc-distribution spec: secretName: fc-distribution-tls-secret issuerRef: name: step-ca-acme kind: ClusterIssuer dnsNames: - dist.iamworkin.lan duration: 2160h # 90d renewBefore: 720h # 30d --- apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: fc-distribution namespace: fc-distribution spec: entryPoints: - websecure routes: - match: Host(`dist.iamworkin.lan`) kind: Rule services: - name: fc-distribution port: 80 tls: secretName: fc-distribution-tls-secret