Files
bluejay-infra/apps/fc-distribution/fc-distribution.yaml

284 lines
11 KiB
YAML

# 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<YYYYMMDD><HHMM>
# 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<tag> -f deploy/Dockerfile.deploy deploy
image: localhost/fc-distribution:v202604232212
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