352 lines
18 KiB
YAML
352 lines
18 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:v202605061948
|
|
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"
|
|
# Public distribution host is GET/HEAD-only at Traefik; this
|
|
# entitlement list controls which editions are readable there.
|
|
- name: FlowerCore__Distribution__EntitlementPublic__PublicEditions__0
|
|
value: "*"
|
|
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
|
|
---
|
|
# === dist.flowercore.io public surface (2026-04-24) =========================
|
|
#
|
|
# Shares the Deployment + Service + PVC with the internal IngressRoute above.
|
|
# The controller's NamedEntitlementResolverRouter picks between the internal
|
|
# (permissive) and public (strict) StaticTokenEntitlementResolver based on
|
|
# the X-FC-Distribution-Profile header — which the middleware below injects
|
|
# on every public-host request after stripping any caller-supplied value.
|
|
#
|
|
# Cert is the shared Cloudflare Origin Certificate for *.flowercore.io, literal
|
|
# bytes copied (matches gitea-public, matrix, telephony, mail, flowercore-landing
|
|
# pattern — not yet via OnePasswordItem operator).
|
|
---
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: cf-origin-flowercore-io
|
|
namespace: fc-distribution
|
|
type: kubernetes.io/tls
|
|
data:
|
|
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVvRENDQTRpZ0F3SUJBZ0lVSXN4c1NKV1VRL0tqZ09ldk81YnNuVi9rZVE4d0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZc3hDekFKQmdOVkJBWVRBbFZUTVJrd0Z3WURWUVFLRXhCRGJHOTFaRVpzWVhKbExDQkpibU11TVRRdwpNZ1lEVlFRTEV5dERiRzkxWkVac1lYSmxJRTl5YVdkcGJpQlRVMHdnUTJWeWRHbG1hV05oZEdVZ1FYVjBhRzl5CmFYUjVNUll3RkFZRFZRUUhFdzFUWVc0Z1JuSmhibU5wYzJOdk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGgKTUI0WERUSTJNRE14TURFMk16TXdNRm9YRFRReE1ETXdOakUyTXpNd01Gb3dZakVaTUJjR0ExVUVDaE1RUTJ4dgpkV1JHYkdGeVpTd2dTVzVqTGpFZE1Cc0dBMVVFQ3hNVVEyeHZkV1JHYkdGeVpTQlBjbWxuYVc0Z1EwRXhKakFrCkJnTlZCQU1USFVOc2IzVmtSbXhoY21VZ1QzSnBaMmx1SUVObGNuUnBabWxqWVhSbE1JSUJJakFOQmdrcWhraUcKOXcwQkFRRUZBQU9DQVE0QU1JSUJDZ0tDQVFFQXV0QmpkQ0xEdHdMQlZCU0Y1ZU1OMkt3ckIxTmZmRVhRMjlRRAo1aVR0dzJFcEZXNVJJSllkMjNrYUpCMU5jZXpHWlg4a0Q0cGEyWHpFZW1MVEtJNWw0MU11b3FoWjczNVE3U3RWCkVjRFFTT2ZYTkZQdFMwb0hqb0pRdGF2QjM0ZmJNR3l4Mmx0MU9HUzRNMGtLUWpBNWR6OTJQYjNyZ1RKR0JhOW4KeTZtVThncjRuUHRSdklxZ3NxdjRtMFA3dVU1YjE3NzU1Y2JLSDVoMzIxWHVjMDU4Tzl4M2JHQ0NuRUJXWDdqeApjRGhkUEs1Ri9XRjVBQnl5cFhIQ0ZxUUd4M1NVbmtCQ0ZQSmRabnMra3BHVUZWZGhud3B6NjBtNnlJSzQ0eVR4CjZqR3JOTFEyM1dOK2gwU1lCZU5vb2JBWThydkpiVlZEaGJqSVhBTWtFNGQzVll1TlhRSURBUUFCbzRJQklqQ0MKQVI0d0RnWURWUjBQQVFIL0JBUURBZ1dnTUIwR0ExVWRKUVFXTUJRR0NDc0dBUVVGQndNQ0JnZ3JCZ0VGQlFjRApBVEFNQmdOVkhSTUJBZjhFQWpBQU1CMEdBMVVkRGdRV0JCUkt1NkJVUDZ0N2dpbFRPay9FdEdKQ3R6N3dTREFmCkJnTlZIU01FR0RBV2dCUWs2Rk5YWFh3MFFJZXA2NVRidXVFV2VQd3BwREJBQmdnckJnRUZCUWNCQVFRME1ESXcKTUFZSUt3WUJCUVVITUFHR0pHaDBkSEE2THk5dlkzTndMbU5zYjNWa1pteGhjbVV1WTI5dEwyOXlhV2RwYmw5agpZVEFqQmdOVkhSRUVIREFhZ2d3cUxtbGhiWGR2Y21zdWFXNkNDbWxoYlhkdmNtc3VhVzR3T0FZRFZSMGZCREV3Ckx6QXRvQ3VnS1lZbmFIUjBjRG92TDJOeWJDNWpiRzkxWkdac1lYSmxMbU52YlM5dmNtbG5hVzVmWTJFdVkzSnMKTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFDSjMvTGNleE5pb0lWdUxoemhmbTZCeDV2SWk3T25CaHF1WUlDdwplNnArZ0prdE16ZFJQcDV0bk03dllBWmxMajVJOTByWDRuczhJc3dEbzJBN2wwYTRGZVJFclFmRklsZXQzbjIyCjUxVTZYVElCSks5c1FZT0FkU3pJUzV1OUNKSFpBUTF5WmxSd3BBR3RVWnhxL1dpcGFWUTRwNXhrcEJNMVlZSlAKNW1jQ09HcFErSnpORlpQc2daYUJncDBYL1BBZkNJRkkyZld5QWE2elBqRm0rdDVXUXIrZlBaT2VUS2VIbWVzVgo3UlZxUUdEb3Q0eTY1NklEdmdmU2ZLRnFIRW9XNDJVbDBxQ05hMS9keEJld3NIS1VWWE1ETkdiQlNVQjM4TG9YCm1OQ3hJQlVOUjR0TG1CQUxZT3hVMnZhSWRCd0xBc2YrcndnVnVjUGpCUTc2VWMwUQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
|
|
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzYwR04wSXNPM0FzRlUKRklYbDR3M1lyQ3NIYTE5OFJkRGIxQVBtSk8zRFlTa1ZibEVnbGgzYmVSb2tIVTF4N01abGZ5UVBpbHJaZk1SNgpZdE1vam1YalV5NmlxRm52ZmxEdEsxVVJ3TkJJNTljMFUrMUxTZ2VPZ2xDMXE4SGZoOXN3YkxIYVczVTRaTGd6ClNRcENNRGwzUDNZOXZldUJNa1lGcjJmTHFaVHlDdmljKzFHOGlxQ3lxL2liUS91NVRsdlh2dm5seHNvZm1IZmIKVmU1elRudzczSGRzWUlLY1FGWmZ1UEZ3T0YwOHJrWDlZWGtBSExLbGNjSVdwQWJIZEpTZVFFSVU4bDFtZXo2UwprWlFWVjJHZkNuUHJTYnJJZ3JqakpQSHFNYXMwdERiZFkzNkhSSmdGNDJpaHNCanl1OGx0VlVPRnVNaGNBeVFUCmgzZFZpNDFkQWdNQkFBRUNnZ0VBTGlseXZkNmVTcEYvZUxtV2lhTVV4NUxwa2dhWHpITkxCQnNNZUpqcytLL0EKVVdlZ1crTkVUdmlLalZ5QlI5SzRocG1IYldDa2lPUDBBQUwrQnlKQ3lvekNOQmJTSEdRejlwc1R5dzZBV1ZlUwpuYjlVWGx1VmFQRktKTTRqbXNydERuYjVic25WT2lGblErTDdTalkwNlFMUlFybjBvUWp0ZFJldUdBMFlQVU90CkhSYzNsMFg2ZHJqdkJYY2prWTQwWm9ZYkRrelJnU1JWbWVOUGFIbjZPR0NtYUVUMXVyK01qYVZ2ME9lbEdIWncKVzljSEIxaHNxRzUvMWU3V0RQN0l0cjkwTmg4ay81NVhiK3lQUnhsRFd5bWtZMzIvdFBtZzdESTRKV2tRRWt3cgpIZUtwODVTcE5ta1liRnVpVFppeU8zZDZ0aXZHNHhFZW8rSzFVVFU4c1FLQmdRRFRNSEU1RDFYVC9HbGR5VHNsCllrODRVL1N0NXUrK2RIUEt1Wmw2dVB0UGgxV1lrdnFRcmdrL05YanVud2xGN0Y3b2tWOGdPeWxreTYwYTZkcXIKeXZwN1ZJdXYzekVlc2h2NjNWMlpaVkMzcXZYSzFheit3Zmx3NitCZmVuRlY5S2NENHN0dTdwOFRPWmFGN01CUgo3YXZzaXVXbWtqdmM1TlVLRmVDRTY0SnZFUUtCZ1FEaWMrbWlNLzBodDN1ajhuOXgyMDFQZFNqbEpVaUc1NjNNCnRYZlBCdDJRT0NhaVluUFNFdTdXdm5pQWRFL2xrMm91cFRWam9LYmZPbDFyQjd6UzVhc2kxdVdDZDhlUy9UWGIKdU5iRmlNMDB4L3JxalMydCtQbTd4MVhrYTB4TFNSRDNmZ0tSQldSN3pscStkYWZ1WE1qelUxRnh5dTIycGphRgpIMEl3NEpCUmpRS0JnUUNOaWhMb0Rob1V5RCtKNXJzb00vb3FJMEtDWnB0WlJzendHbkg5cVFwdFk2Ti9iVXBYCk92emhpeUh3czAvUXVEbG5uejVrNktHMmR6Y2VLWXN2eGdzWUt6S3ZmV043VWgya2hVWWM3NlVvWTREMkh6MGgKUkxtNzc2cGg4enNRUTdiSHlQRlUrTUpPYlRNdnNOdTRUUlVEcEplRGl0QnFIRWVYeWMrKzVlUjJNUUtCZ0h2UgptVHVoWlpVYitEVEtrVGkyQ20yWnlBU1RBRGNUVW9xTjVyYUNNSDk4MUZNUnRmWjFkN1pmYXhBQmlQWWtSbmkrCnlKUnk4UXM1cEg2ek9tR3VSb2JFTGJYS3ZJcjRmSXhwWXJXYmVXaVV0L09yd2dCUUZHekNMNHEzeUgyWnMvYy8KSlRRYVdMa0JPY2pPR0VaUzRXVjZkeHZiTTJNZE9zNUxLeXdDZmFhNUFvR0FIQUE1eEN0dndOZE4xeExndkZ3RApPK2lyMDl1bXMxOFBzSVpmK1ZrWGtpcHF4MWNUT0hEanpPR01yWXV0M2FFeE00Zjd2ckFHRFMyY2pwZjM0T1JxCit4Y2gwWlNaQ2FDZmlnZG9OelNkcDFLcmo0cnFKdG5ZdS9CNDlDQlVoSDBNaCtSRWswQ0hHOVE4b3FOWFk0V0wKbVVOVTZMYUkwQWtvSzNVb2tWQVJEYXM9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
|
|
---
|
|
# Traefik middleware: strips any caller-supplied X-FC-Distribution-Profile,
|
|
# then sets an authoritative 'public' value so the controller routes to the
|
|
# strict entitlement resolver. The trust boundary is this middleware — the
|
|
# internal IngressRoute (dist.iamworkin.lan) does NOT attach it.
|
|
apiVersion: traefik.io/v1alpha1
|
|
kind: Middleware
|
|
metadata:
|
|
name: dist-public-profile-header
|
|
namespace: fc-distribution
|
|
spec:
|
|
headers:
|
|
customRequestHeaders:
|
|
X-FC-Distribution-Profile: "public"
|
|
---
|
|
# Public IngressRoute: binds dist.flowercore.io (Cloudflare-proxied A record
|
|
# -> pfSense NAT -> Traefik VIP 10.0.56.200) to the same backend Service that
|
|
# serves dist.iamworkin.lan. Header-injection middleware ensures the
|
|
# controller uses the public (strict) entitlement resolver.
|
|
apiVersion: traefik.io/v1alpha1
|
|
kind: IngressRoute
|
|
metadata:
|
|
name: fc-distribution-public
|
|
namespace: fc-distribution
|
|
spec:
|
|
entryPoints:
|
|
- websecure
|
|
routes:
|
|
# Method allowlist: Host + (GET || HEAD). Anything else misses every
|
|
# route and Traefik returns 404 before reaching the pod — edge-level
|
|
# defense-in-depth over the controller's strict-mode entitlement check.
|
|
# Together these block admin ops (POST /blobs, POST /manifests*) from
|
|
# ever being processed on the public surface.
|
|
- match: Host(`dist.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
|
kind: Rule
|
|
middlewares:
|
|
- name: dist-public-profile-header
|
|
services:
|
|
- name: fc-distribution
|
|
port: 80
|
|
tls:
|
|
secretName: cf-origin-flowercore-io
|