Compare commits
66 Commits
codex/whc4
...
gx10-gitop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c369cc7ec | ||
|
|
299ce5aeed | ||
|
|
57a1afe159 | ||
|
|
0d71a789c2 | ||
|
|
14d89ba49d | ||
|
|
0eda4362ce | ||
|
|
6f12ace02d | ||
|
|
0c03e53df9 | ||
|
|
62a3e75ddc | ||
|
|
4bbd157c8f | ||
|
|
1969285e4f | ||
|
|
68a5f1ac5d | ||
|
|
f0b122bac7 | ||
|
|
c9538eeeef | ||
|
|
c968e1c4d9 | ||
|
|
bc39da26a1 | ||
|
|
984e3423db | ||
|
|
5d0baa0fdd | ||
|
|
f594d82c65 | ||
|
|
0b7d0fa476 | ||
|
|
500b2484ab | ||
|
|
c0a0341cef | ||
|
|
adafbb41f7 | ||
|
|
09dce583bb | ||
|
|
6d0464ec17 | ||
|
|
3b96a6272a | ||
|
|
061a0d61a8 | ||
|
|
ae6dfe9144 | ||
|
|
28f9ac2ef9 | ||
|
|
a7ba47e307 | ||
|
|
2e8cabcd63 | ||
|
|
3948350ac2 | ||
|
|
ac153248c2 | ||
|
|
9cef99739a | ||
|
|
bd050c3d9b | ||
|
|
a41b22bca4 | ||
|
|
38590d3d5a | ||
|
|
27815cefca | ||
|
|
6e0d33b5b9 | ||
|
|
b015c8a8e1 | ||
|
|
d51e55c78d | ||
|
|
f78e6747b4 | ||
|
|
e543018bdc | ||
|
|
d0c9717d90 | ||
|
|
2c1aa3f0c8 | ||
|
|
aba9d7c995 | ||
|
|
a56e98422f | ||
|
|
27600b8b99 | ||
|
|
9929a91812 | ||
|
|
5af4d9077a | ||
|
|
efa0434b9b | ||
|
|
ad709e2317 | ||
|
|
f636b5092c | ||
|
|
82d9d66f62 | ||
|
|
8b1f8df3dd | ||
|
|
65af283aea | ||
|
|
b7d34da3d6 | ||
|
|
63fde0a593 | ||
|
|
764e4a8f49 | ||
|
|
3bdb9eee81 | ||
|
|
83db2bbe6b | ||
|
|
c32327cdee | ||
|
|
818463562e | ||
|
|
f821c0e661 | ||
|
|
6c519bfd6a | ||
|
|
ee14d3a2d0 |
@@ -101,7 +101,7 @@ curl -sk -X DELETE https://dns.iamworkin.lan/api/v1/servers/<serverId>/zones/iam
|
||||
- **StatefulSet PVC drift**: `volumeClaimTemplates` needs explicit `volumeMode: Filesystem` or ArgoCD SSA self-heals forever. See memory `feedback_argocd_statefulset_pvc_drift.md`.
|
||||
- **IngressRoute namespace split**: this RKE2 Traefik install does not allow cross-namespace service refs. Keep the `IngressRoute`, backend `Service`, and TLS secret in the same namespace; if one host is shared across namespaces, duplicate the `Certificate` and move the route next to the destination service.
|
||||
- **Public read-only hosts**: if a public host fronts a service that also exposes admin writes internally, add a Traefik route match like `Host(...) && (Method(GET) || Method(HEAD))` on the public edge instead of trusting the app to reject unsafe methods.
|
||||
- **Public read-write allowlist hosts**: if a public host accepts a tightly bounded write surface (e.g. bootstrap-JWT POST), pin the allowlist as `(Method(GET) || Method(HEAD) || Method(POST) || Method(OPTIONS))`. PUT/PATCH/DELETE must still 404 at the route. Track A's `updatecenter.iamworkin.lan` / `updates.iamworkin.lan` are the canonical example. The lint test enforces this invariant.
|
||||
- **Public read-write allowlist hosts**: if a public host accepts a tightly bounded write surface (e.g. bootstrap-JWT POST), pin the allowlist as `(Method(GET) || Method(HEAD) || Method(POST) || Method(OPTIONS))`. PUT/PATCH/DELETE must still 404 at the route. Internal UpdateCenter hosts (`updatecenter.iamworkin.lan` / `updates.iamworkin.lan`) are the canonical example. Public UpdateCenter delivery hosts (`update.flowercore.io` / `updates.flowercore.io`) stay GET/HEAD-only and share-link gated until an explicit operator decision changes that posture.
|
||||
- **Traefik VIP netpols**: when a `NetworkPolicy` allows `10.0.56.200`, also allow the post-DNAT backend ports (`8443` for TLS plus `8080` or `8000` for HTTP) or Calico will drop the rewritten flow.
|
||||
- **Auth-safe probes**: services behind API-key or global auth middleware should prefer `tcpSocket` probes unless `/health` is explicitly exempted before the middleware runs.
|
||||
- **ArgoCD must use internal Gitea URL**: `http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git`, not the external HTTPS URL (step-ca cert isn't trusted by ArgoCD). The `ApplicationSet` and any hand-created `Application` must both use the internal URL.
|
||||
|
||||
61
apps-gx10/fc-apple-mdm/README.md
Normal file
61
apps-gx10/fc-apple-mdm/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# FlowerCore Apple MDM on GX10
|
||||
|
||||
This directory deploys the NanoHUB `v0.2.0` substrate for Apple MDM protocol
|
||||
traffic at `https://mdm.iamworkin.lan`.
|
||||
|
||||
## Runtime
|
||||
|
||||
- Namespace: `fc-apple-mdm`
|
||||
- Image: `localhost/fc-apple-mdm-nanohub:v0.2.0-20260617`
|
||||
- Upstream digest: `ghcr.io/micromdm/nanohub:latest@sha256:e36a50db2dc3d2bf736645e58712f622c04b05b28487390981905ef4d0be5fbd`
|
||||
- Persistent state: `fc-apple-mdm-data` on `local-path`, mounted at `/var/lib/nanohub`
|
||||
- File backend DSN: `/var/lib/nanohub/db`
|
||||
- Required secret: `Secret/fc-apple-mdm-runtime`, key `NANOHUB_API_KEY`
|
||||
- Optional later bridge secret: `NANOHUB_WEBHOOK_URL`
|
||||
- Required CA mount: `ConfigMap/fc-apple-mdm-root-ca`, key `root_ca.crt`
|
||||
- SCEP backend: noc1 systemd service `step-ca-apple-mdm-scep`, forwarded through
|
||||
selectorless `Service/fc-apple-mdm-scep` and `EndpointSlice/fc-apple-mdm-scep-noc1`
|
||||
to `10.0.56.10:9080`
|
||||
|
||||
NanoHUB API authentication is HTTP Basic with username `nanohub` and password
|
||||
from `NANOHUB_API_KEY`.
|
||||
|
||||
## Public Surface
|
||||
|
||||
The Traefik route intentionally exposes only:
|
||||
|
||||
- `/version`
|
||||
- `/mdm`
|
||||
- `/checkin`
|
||||
- `/scep`
|
||||
|
||||
NanoHUB APIs under `/api/v1/*` stay cluster-internal for MDM-N1. The
|
||||
DeviceManagement bridge can use the ClusterIP service directly once its NanoHUB
|
||||
client lane lands.
|
||||
|
||||
SCEP is backed by the dedicated Apple-MDM-specific RSA step-ca hierarchy on
|
||||
noc1, not by the IAmWorkin ACME CA. The live profile URL is:
|
||||
|
||||
```text
|
||||
https://mdm.iamworkin.lan/scep/apple-mdm-scep
|
||||
```
|
||||
|
||||
Do not point `APPLE_MDM_SCEP_URL` at a placeholder URL or at the ECDSA
|
||||
IAmWorkin ACME CA; Smallstep SCEP requires an RSA intermediate/decrypter path.
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
1. Create or refresh the runtime Kubernetes Secret from the 1Password item
|
||||
`FlowerCore Apple MDM Runtime` before sync. GX10 does not yet depend on the
|
||||
1Password operator for this workload.
|
||||
2. Import `localhost/fc-apple-mdm-nanohub:v0.2.0-20260617` into GX10 containerd
|
||||
before ArgoCD syncs. The deployment uses `imagePullPolicy: Never`.
|
||||
3. Ensure `mdm.iamworkin.lan` resolves to the GX10 Traefik VIP `10.0.57.202`
|
||||
before cert-manager requests `Certificate/fc-apple-mdm-tls`.
|
||||
4. Prove `https://mdm.iamworkin.lan/version` after ArgoCD converges.
|
||||
5. Prove SCEP CA publication with
|
||||
`curl -sk -o /dev/null -w '%{http_code} %{size_download}\n' 'https://mdm.iamworkin.lan/scep/apple-mdm-scep?operation=GetCACert'`.
|
||||
|
||||
This lane does not create an APNs MDM push certificate, enrollment profile,
|
||||
managed Wi-Fi payload, managed app install, or supervised iPad enrollment. Those
|
||||
remain MDM-N2 through MDM-N8.
|
||||
322
apps-gx10/fc-apple-mdm/fc-apple-mdm.yaml
Normal file
322
apps-gx10/fc-apple-mdm/fc-apple-mdm.yaml
Normal file
@@ -0,0 +1,322 @@
|
||||
# FlowerCore Apple MDM NanoHUB workload for the GX10 cluster.
|
||||
# Secret values are copied into Kubernetes Secrets out of band until the
|
||||
# 1Password operator exists on GX10; never commit secret data here.
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: fc-apple-mdm
|
||||
labels:
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: fc-apple-mdm-root-ca
|
||||
namespace: fc-apple-mdm
|
||||
data:
|
||||
root_ca.crt: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBxDCCAWqgAwIBAgIRAPY357G6ow6zMAL5+4bS2kkwCgYIKoZIzj0EAwIwQDEa
|
||||
MBgGA1UEChMRSUFtV29ya2luIEFDTUUgQ0ExIjAgBgNVBAMTGUlBbVdvcmtpbiBB
|
||||
Q01FIENBIFJvb3QgQ0EwHhcNMjYwMzA4MTgwNzExWhcNMzYwMzA1MTgwNzExWjBA
|
||||
MRowGAYDVQQKExFJQW1Xb3JraW4gQUNNRSBDQTEiMCAGA1UEAxMZSUFtV29ya2lu
|
||||
IEFDTUUgQ0EgUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJ2n04X1
|
||||
JZo5Zdq/i1Idv8+fqwZyAzBh7whbqj0SWsJL8UWRabCMqYCs7+dXO0xRSzqkwFDL
|
||||
x+vooOai8RgRNhajRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/
|
||||
AgEBMB0GA1UdDgQWBBRnuPPQR6iM/H6vOluiU3Sygayz8jAKBggqhkjOPQQDAgNI
|
||||
ADBFAiEArQK9dYPGmAZsdYnjziuFVVE5NKZUcceYvGfGC+tLXUsCIAudF2zJrCRq
|
||||
3mK50ZZET/fwTkJwiEF4824mjP8p1CKM
|
||||
-----END CERTIFICATE-----
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: fc-apple-mdm-data
|
||||
namespace: fc-apple-mdm
|
||||
labels:
|
||||
app: fc-apple-mdm
|
||||
app.kubernetes.io/name: fc-apple-mdm
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: fc-apple-mdm
|
||||
namespace: fc-apple-mdm
|
||||
labels:
|
||||
app: fc-apple-mdm
|
||||
app.kubernetes.io/name: fc-apple-mdm
|
||||
app.kubernetes.io/component: mdm
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 3
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: fc-apple-mdm
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: fc-apple-mdm
|
||||
app.kubernetes.io/name: fc-apple-mdm
|
||||
app.kubernetes.io/component: mdm
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
annotations:
|
||||
fc.flowercore.io/healthz-anon: "true"
|
||||
fc.flowercore.io/probe-path: "/version"
|
||||
flowercore.io/audit-trace-id: "apple-mdm-nanohub-runtime-trace"
|
||||
flowercore.io/root-ca-sha256: "a9120c88fa3ec735d790aa4cfeb61ac2946730338969015bebaccc08fe10535e"
|
||||
prometheus.io/scrape: "false"
|
||||
spec:
|
||||
enableServiceLinks: false
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1654
|
||||
runAsGroup: 1654
|
||||
fsGroup: 1654
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: nanohub
|
||||
image: localhost/fc-apple-mdm-nanohub:v0.2.0-20260617
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 9004
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: HOME
|
||||
value: "/var/lib/nanohub"
|
||||
- name: NANOHUB_LISTEN
|
||||
value: ":9004"
|
||||
- name: NANOHUB_STORAGE
|
||||
value: "file"
|
||||
- name: NANOHUB_STORAGE_DSN
|
||||
value: "/var/lib/nanohub/db"
|
||||
- name: NANOHUB_CHECKIN
|
||||
value: "true"
|
||||
- name: NANOHUB_CA
|
||||
value: "/etc/nanohub/ca/root_ca.crt"
|
||||
- name: NANOHUB_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: fc-apple-mdm-runtime
|
||||
key: NANOHUB_API_KEY
|
||||
- name: NANOHUB_WEBHOOK_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: fc-apple-mdm-runtime
|
||||
key: NANOHUB_WEBHOOK_URL
|
||||
optional: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /version
|
||||
port: 9004
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /version
|
||||
port: 9004
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 9004
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
failureThreshold: 3
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1654
|
||||
runAsGroup: 1654
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/nanohub
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: root-ca
|
||||
mountPath: /etc/nanohub/ca
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: fc-apple-mdm-data
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: root-ca
|
||||
configMap:
|
||||
name: fc-apple-mdm-root-ca
|
||||
items:
|
||||
- key: root_ca.crt
|
||||
path: root_ca.crt
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: fc-apple-mdm
|
||||
namespace: fc-apple-mdm
|
||||
labels:
|
||||
app: fc-apple-mdm
|
||||
app.kubernetes.io/name: fc-apple-mdm
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: fc-apple-mdm
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 9004
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: fc-apple-mdm-scep
|
||||
namespace: fc-apple-mdm
|
||||
labels:
|
||||
app: fc-apple-mdm-scep
|
||||
app.kubernetes.io/name: fc-apple-mdm-scep
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 9080
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: discovery.k8s.io/v1
|
||||
kind: EndpointSlice
|
||||
metadata:
|
||||
name: fc-apple-mdm-scep-noc1
|
||||
namespace: fc-apple-mdm
|
||||
labels:
|
||||
kubernetes.io/service-name: fc-apple-mdm-scep
|
||||
app.kubernetes.io/name: fc-apple-mdm-scep
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
addressType: IPv4
|
||||
endpoints:
|
||||
- addresses:
|
||||
- 10.0.56.10
|
||||
conditions:
|
||||
ready: true
|
||||
ports:
|
||||
- name: http
|
||||
port: 9080
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: fc-apple-mdm-tls
|
||||
namespace: fc-apple-mdm
|
||||
annotations:
|
||||
flowercore.io/dns-preflight: "mdm.iamworkin.lan must resolve to 10.0.57.202 before ACME sync"
|
||||
spec:
|
||||
secretName: fc-apple-mdm-tls
|
||||
issuerRef:
|
||||
name: step-ca-acme
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- mdm.iamworkin.lan
|
||||
duration: 720h
|
||||
renewBefore: 240h
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: fc-apple-mdm
|
||||
namespace: fc-apple-mdm
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`mdm.iamworkin.lan`) && PathPrefix(`/scep`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: fc-apple-mdm-scep
|
||||
port: 80
|
||||
- match: Host(`mdm.iamworkin.lan`) && (PathPrefix(`/mdm`) || PathPrefix(`/checkin`) || PathPrefix(`/version`))
|
||||
kind: Rule
|
||||
services:
|
||||
- name: fc-apple-mdm
|
||||
port: 80
|
||||
tls:
|
||||
secretName: fc-apple-mdm-tls
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: fc-apple-mdm-netpol
|
||||
namespace: fc-apple-mdm
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: fc-apple-mdm
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: traefik-system
|
||||
ports:
|
||||
- port: 9004
|
||||
protocol: TCP
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: fc-devicemgmt
|
||||
ports:
|
||||
- port: 9004
|
||||
protocol: TCP
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- port: 53
|
||||
protocol: UDP
|
||||
- port: 53
|
||||
protocol: TCP
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
ports:
|
||||
- port: 443
|
||||
protocol: TCP
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: fc-devicemgmt
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
6
apps-gx10/fc-apple-mdm/kustomization.yaml
Normal file
6
apps-gx10/fc-apple-mdm/kustomization.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# ArgoCD discovers apps-gx10/* directories on the GX10 GitOps branch.
|
||||
# This kustomization is for local previews and single-app validation.
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- fc-apple-mdm.yaml
|
||||
@@ -83,7 +83,7 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"image": "localhost/fc-chat-web:v20260617-chatfix-54fd549",
|
||||
"image": "localhost/fc-chat-web:v20260619-sec3-5a8859b",
|
||||
"imagePullPolicy": "Never",
|
||||
"livenessProbe": {
|
||||
"failureThreshold": 3,
|
||||
@@ -98,22 +98,22 @@
|
||||
"timeoutSeconds": 5
|
||||
},
|
||||
"name": "chat-web",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 8080,
|
||||
"name": "http",
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"securityContext": {
|
||||
"allowPrivilegeEscalation": false,
|
||||
"capabilities": {
|
||||
"drop": [
|
||||
"ALL"
|
||||
]
|
||||
},
|
||||
"readOnlyRootFilesystem": true
|
||||
},
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 8080,
|
||||
"name": "http",
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"securityContext": {
|
||||
"allowPrivilegeEscalation": false,
|
||||
"capabilities": {
|
||||
"drop": [
|
||||
"ALL"
|
||||
]
|
||||
},
|
||||
"readOnlyRootFilesystem": true
|
||||
},
|
||||
"readinessProbe": {
|
||||
"failureThreshold": 6,
|
||||
"httpGet": {
|
||||
@@ -138,49 +138,49 @@
|
||||
},
|
||||
"terminationMessagePath": "/dev/termination-log",
|
||||
"terminationMessagePolicy": "File",
|
||||
"volumeMounts": [
|
||||
{
|
||||
"mountPath": "/data",
|
||||
"name": "data"
|
||||
},
|
||||
{
|
||||
"mountPath": "/tmp",
|
||||
"name": "temp"
|
||||
},
|
||||
{
|
||||
"mountPath": "/app/logs",
|
||||
"name": "logs"
|
||||
}
|
||||
]
|
||||
"volumeMounts": [
|
||||
{
|
||||
"mountPath": "/data",
|
||||
"name": "data"
|
||||
},
|
||||
{
|
||||
"mountPath": "/tmp",
|
||||
"name": "temp"
|
||||
},
|
||||
{
|
||||
"mountPath": "/app/logs",
|
||||
"name": "logs"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"dnsPolicy": "ClusterFirst",
|
||||
"restartPolicy": "Always",
|
||||
"schedulerName": "default-scheduler",
|
||||
"securityContext": {
|
||||
"fsGroup": 1654,
|
||||
"fsGroupChangePolicy": "OnRootMismatch",
|
||||
"runAsGroup": 1654,
|
||||
"runAsNonRoot": true,
|
||||
"runAsUser": 1654
|
||||
},
|
||||
"securityContext": {
|
||||
"fsGroup": 1654,
|
||||
"fsGroupChangePolicy": "OnRootMismatch",
|
||||
"runAsGroup": 1654,
|
||||
"runAsNonRoot": true,
|
||||
"runAsUser": 1654
|
||||
},
|
||||
"terminationGracePeriodSeconds": 30,
|
||||
"volumes": [
|
||||
{
|
||||
"name": "data",
|
||||
"persistentVolumeClaim": {
|
||||
"claimName": "chat-web-data"
|
||||
}
|
||||
},
|
||||
{
|
||||
"emptyDir": {},
|
||||
"name": "temp"
|
||||
},
|
||||
{
|
||||
"emptyDir": {},
|
||||
"name": "logs"
|
||||
}
|
||||
]
|
||||
{
|
||||
"name": "data",
|
||||
"persistentVolumeClaim": {
|
||||
"claimName": "chat-web-data"
|
||||
}
|
||||
},
|
||||
{
|
||||
"emptyDir": {},
|
||||
"name": "temp"
|
||||
},
|
||||
{
|
||||
"emptyDir": {},
|
||||
"name": "logs"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
65
apps-gx10/fc-devicemgmt/README.md
Normal file
65
apps-gx10/fc-devicemgmt/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# FlowerCore DeviceManagement on GX10
|
||||
|
||||
This adopted GX10 app hosts `FlowerCore.DeviceManagement.Web` at
|
||||
`https://devices.iamworkin.lan`. Agent-only REST/SignalR callbacks can use
|
||||
`https://devices-agent.iamworkin.lan`, which is a separate Traefik router that
|
||||
requires a TLS client certificate and forwards the presented certificate to the
|
||||
app. Traefik v3.6 currently forwards raw base64 DER in
|
||||
`X-Forwarded-Tls-Client-Cert`; the app also accepts URL-escaped PEM for
|
||||
compatibility with older/alternate Traefik shapes.
|
||||
|
||||
## Apple MDM Runtime Contract
|
||||
|
||||
Apple MDM is enabled in NanoHUB mode, but enrollment remains unavailable until
|
||||
the runtime secret contains real Apple-side material. Do not use placeholder
|
||||
values to clear readiness checks.
|
||||
|
||||
`Secret/fc-devicemgmt-runtime` supports these Apple MDM keys:
|
||||
|
||||
| Key | Purpose |
|
||||
| --- | --- |
|
||||
| `DEVICE_MANAGEMENT_OPERATOR_API_KEY` | Required operator API key for authenticated REST/MCP write operations, including Android command queueing. |
|
||||
| `DEVICE_MANAGEMENT_ADMIN_API_KEY` | Required admin API key for privileged DeviceManagement operations. |
|
||||
| `DEVICE_MANAGEMENT_AGENT_API_KEY` | Required scoped agent credential for REST agent callbacks when TLS terminates before Kestrel; maps to `Auth:AgentApiKey` and `FlowerCore:Auth:AgentApiKey`. |
|
||||
| `NANOHUB_API_KEY` | NanoHUB API password for HTTP Basic user `nanohub`. |
|
||||
| `APPLE_MDM_APNS_TOPIC` | MDM APNs topic returned after uploading the Apple MDM push certificate to NanoHUB/NanoMDM. |
|
||||
| `APPLE_MDM_SCEP_URL` | Live SCEP URL included in the enrollment profile. |
|
||||
| `APPLE_MDM_SCEP_CHALLENGE` | SCEP challenge shared with the SCEP provisioner. |
|
||||
| `APPLE_MDM_PROFILE_SIGNING_CERTIFICATE_PEM` | PEM certificate used to CMS-sign `.mobileconfig` profiles. |
|
||||
| `APPLE_MDM_PROFILE_SIGNING_PRIVATE_KEY_PEM` | PEM private key matching the profile-signing certificate. |
|
||||
| `APPLE_MDM_REQUIRE_MANAGED_WIFI_PAYLOAD` | Set to `true` only when Wi-Fi payload delivery should gate enrollment readiness. |
|
||||
| `APPLE_MDM_MANAGED_WIFI_SSID` | Managed Wi-Fi SSID for the iPad profile. |
|
||||
| `APPLE_MDM_MANAGED_WIFI_PASSWORD` | Managed Wi-Fi password when the network is not open. |
|
||||
|
||||
Non-secret profile constants stay in GitOps: NanoHUB base URL, MDM server URL,
|
||||
check-in URL, organization/display names, the HTTPS trust anchor certificate,
|
||||
managed Wi-Fi encryption type, auto-join, and MAC-randomization disablement.
|
||||
|
||||
DeviceManagement auth is enabled on GX10. The deployment maps
|
||||
`DEVICE_MANAGEMENT_OPERATOR_API_KEY` to both `Auth__ApiKey` and
|
||||
`FlowerCore__Auth__ApiKey`; the unprefixed key keeps the MCP API key post-config
|
||||
path aligned with REST auth. Agent heartbeat, inventory, command poll, app-catalog,
|
||||
and command-result callbacks use the agent-specific authorization boundary: the
|
||||
server validates a direct device client certificate when Kestrel receives one,
|
||||
validates Traefik-forwarded client certificates only on
|
||||
`devices-agent.iamworkin.lan`, and also accepts only the scoped
|
||||
`DEVICE_MANAGEMENT_AGENT_API_KEY` via `Authorization: Bearer` or
|
||||
`X-Agent-Api-Key` as the fallback path. Operator write endpoints must use
|
||||
`X-Api-Key`.
|
||||
|
||||
The agent-only Traefik route currently uses `RequireAnyClientCert`; the
|
||||
application remains the authorization boundary by matching the forwarded client
|
||||
certificate thumbprint to the enrolled device record. Once DeviceManagement
|
||||
exports a persistent enrollment CA bundle, switch this TLSOption to
|
||||
`RequireAndVerifyClientCert` with that CA secret.
|
||||
|
||||
## Readiness Check
|
||||
|
||||
After changing the runtime secret and letting the pod roll, verify:
|
||||
|
||||
```bash
|
||||
curl -sk https://devices.iamworkin.lan/api/v1/apple-mdm/enrollment-profile/status
|
||||
```
|
||||
|
||||
Configurator enrollment must wait until this status reports `available=true`
|
||||
and an empty `missingRequirements` array.
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"apiVersion": "cert-manager.io/v1",
|
||||
"kind": "Certificate",
|
||||
"metadata": {
|
||||
"name": "fc-devicemgmt-agent-tls",
|
||||
"namespace": "fc-devicemgmt"
|
||||
},
|
||||
"spec": {
|
||||
"dnsNames": [
|
||||
"devices-agent.iamworkin.lan"
|
||||
],
|
||||
"issuerRef": {
|
||||
"kind": "ClusterIssuer",
|
||||
"name": "step-ca-acme"
|
||||
},
|
||||
"secretName": "fc-devicemgmt-agent-tls"
|
||||
}
|
||||
}
|
||||
@@ -88,21 +88,240 @@
|
||||
"name": "FlowerCore__Database__ConnectionStrings__Sqlite",
|
||||
"value": "Data Source=/data/devicemgmt.db"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__Database__Password",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "DB-Password",
|
||||
"name": "fc-devicemgmt-runtime"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__EventBus__Redis__Configuration",
|
||||
"value": "redis.fc-redis.svc:6379"
|
||||
}
|
||||
],
|
||||
"image": "localhost/fc-devicemgmt-web:v20260617-an13-b9c79c4",
|
||||
{
|
||||
"name": "FlowerCore__Database__Password",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "DB-Password",
|
||||
"name": "fc-devicemgmt-runtime"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__Auth__Enabled",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "Auth__ApiKey",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "DEVICE_MANAGEMENT_OPERATOR_API_KEY",
|
||||
"name": "fc-devicemgmt-runtime"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__Auth__ApiKey",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "DEVICE_MANAGEMENT_OPERATOR_API_KEY",
|
||||
"name": "fc-devicemgmt-runtime"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Auth__AdminApiKey",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "DEVICE_MANAGEMENT_ADMIN_API_KEY",
|
||||
"name": "fc-devicemgmt-runtime"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__Auth__AdminApiKey",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "DEVICE_MANAGEMENT_ADMIN_API_KEY",
|
||||
"name": "fc-devicemgmt-runtime"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Auth__AgentApiKey",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "DEVICE_MANAGEMENT_AGENT_API_KEY",
|
||||
"name": "fc-devicemgmt-runtime"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__Auth__AgentApiKey",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "DEVICE_MANAGEMENT_AGENT_API_KEY",
|
||||
"name": "fc-devicemgmt-runtime"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AgentMtls__ForwardedCertificateHosts__0",
|
||||
"value": "devices-agent.iamworkin.lan"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AgentMtls__ForwardedCertificateHeader",
|
||||
"value": "X-Forwarded-Tls-Client-Cert"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__EventBus__Redis__Configuration",
|
||||
"value": "redis.fc-redis.svc:6379"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__Enabled",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__GatewayMode",
|
||||
"value": "nanohub"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__NanoHubBaseUrl",
|
||||
"value": "http://fc-apple-mdm.fc-apple-mdm.svc"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__NanoHubApiUserName",
|
||||
"value": "nanohub"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__NanoHubNanoMdmApiPath",
|
||||
"value": "/api/v1/nanomdm/"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__EnrollmentProfileDownloadUrl",
|
||||
"value": "https://devices.iamworkin.lan/api/v1/apple-mdm/enrollment-profile.mobileconfig"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__MdmServerUrl",
|
||||
"value": "https://mdm.iamworkin.lan/mdm"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__MdmCheckInUrl",
|
||||
"value": "https://mdm.iamworkin.lan/checkin"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__Organization",
|
||||
"value": "FlowerCore"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__EnrollmentDisplayName",
|
||||
"value": "FlowerCore Apple MDM"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__ScepName",
|
||||
"value": "FlowerCore Apple MDM Device Identity"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__TrustAnchorDisplayName",
|
||||
"value": "IAmWorkin ACME CA Root CA"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__TrustAnchorCertificatePem",
|
||||
"value": "-----BEGIN CERTIFICATE-----\nMIIBxDCCAWqgAwIBAgIRAPY357G6ow6zMAL5+4bS2kkwCgYIKoZIzj0EAwIwQDEa\nMBgGA1UEChMRSUFtV29ya2luIEFDTUUgQ0ExIjAgBgNVBAMTGUlBbVdvcmtpbiBB\nQ01FIENBIFJvb3QgQ0EwHhcNMjYwMzA4MTgwNzExWhcNMzYwMzA1MTgwNzExWjBA\nMRowGAYDVQQKExFJQW1Xb3JraW4gQUNNRSBDQTEiMCAGA1UEAxMZSUFtV29ya2lu\nIEFDTUUgQ0EgUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJ2n04X1\nJZo5Zdq/i1Idv8+fqwZyAzBh7whbqj0SWsJL8UWRabCMqYCs7+dXO0xRSzqkwFDL\nx+vooOai8RgRNhajRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/\nAgEBMB0GA1UdDgQWBBRnuPPQR6iM/H6vOluiU3Sygayz8jAKBggqhkjOPQQDAgNI\nADBFAiEArQK9dYPGmAZsdYnjziuFVVE5NKZUcceYvGfGC+tLXUsCIAudF2zJrCRq\n3mK50ZZET/fwTkJwiEF4824mjP8p1CKM\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__NanoHubApiKey",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "NANOHUB_API_KEY",
|
||||
"name": "fc-devicemgmt-runtime"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__ApnsTopic",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "APPLE_MDM_APNS_TOPIC",
|
||||
"name": "fc-devicemgmt-runtime",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__ScepUrl",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "APPLE_MDM_SCEP_URL",
|
||||
"name": "fc-devicemgmt-runtime",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__ScepChallenge",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "APPLE_MDM_SCEP_CHALLENGE",
|
||||
"name": "fc-devicemgmt-runtime",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__ProfileSigningCertificatePem",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "APPLE_MDM_PROFILE_SIGNING_CERTIFICATE_PEM",
|
||||
"name": "fc-devicemgmt-runtime",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__ProfileSigningPrivateKeyPem",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "APPLE_MDM_PROFILE_SIGNING_PRIVATE_KEY_PEM",
|
||||
"name": "fc-devicemgmt-runtime",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__RequireManagedWifiPayload",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "APPLE_MDM_REQUIRE_MANAGED_WIFI_PAYLOAD",
|
||||
"name": "fc-devicemgmt-runtime",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__ManagedWifiSsid",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "APPLE_MDM_MANAGED_WIFI_SSID",
|
||||
"name": "fc-devicemgmt-runtime",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__ManagedWifiPassword",
|
||||
"valueFrom": {
|
||||
"secretKeyRef": {
|
||||
"key": "APPLE_MDM_MANAGED_WIFI_PASSWORD",
|
||||
"name": "fc-devicemgmt-runtime",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__ManagedWifiEncryptionType",
|
||||
"value": "WPA2"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__ManagedWifiAutoJoin",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__DeviceManagement__AppleMdm__ManagedWifiDisableAssociationMacRandomization",
|
||||
"value": "true"
|
||||
}
|
||||
],
|
||||
"image": "localhost/fc-devicemgmt-web:v20260619-mtlsder-5131f32",
|
||||
"imagePullPolicy": "Never",
|
||||
"livenessProbe": {
|
||||
"failureThreshold": 3,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"apiVersion": "traefik.io/v1alpha1",
|
||||
"kind": "IngressRoute",
|
||||
"metadata": {
|
||||
"name": "devicemgmt-agent-mtls",
|
||||
"namespace": "fc-devicemgmt"
|
||||
},
|
||||
"spec": {
|
||||
"entryPoints": [
|
||||
"websecure"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"kind": "Rule",
|
||||
"match": "Host(`devices-agent.iamworkin.lan`)",
|
||||
"middlewares": [
|
||||
{
|
||||
"name": "devicemgmt-agent-pass-client-cert",
|
||||
"namespace": "fc-devicemgmt"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"name": "fc-devicemgmt-web",
|
||||
"port": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tls": {
|
||||
"options": {
|
||||
"name": "devicemgmt-agent-mtls",
|
||||
"namespace": "fc-devicemgmt"
|
||||
},
|
||||
"secretName": "fc-devicemgmt-agent-tls"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"apiVersion": "traefik.io/v1alpha1",
|
||||
"kind": "Middleware",
|
||||
"metadata": {
|
||||
"name": "devicemgmt-agent-pass-client-cert",
|
||||
"namespace": "fc-devicemgmt"
|
||||
},
|
||||
"spec": {
|
||||
"passTLSClientCert": {
|
||||
"pem": true
|
||||
}
|
||||
}
|
||||
}
|
||||
13
apps-gx10/fc-devicemgmt/tlsoption-devicemgmt-agent-mtls.json
Normal file
13
apps-gx10/fc-devicemgmt/tlsoption-devicemgmt-agent-mtls.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"apiVersion": "traefik.io/v1alpha1",
|
||||
"kind": "TLSOption",
|
||||
"metadata": {
|
||||
"name": "devicemgmt-agent-mtls",
|
||||
"namespace": "fc-devicemgmt"
|
||||
},
|
||||
"spec": {
|
||||
"clientAuth": {
|
||||
"clientAuthType": "RequireAnyClientCert"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ spec:
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: web
|
||||
image: localhost/fc-gateway:v20260617-hm1-gateway-e0627e3
|
||||
image: localhost/fc-gateway:v20260619-sec3-429e6cf
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
"value": "mysql"
|
||||
}
|
||||
],
|
||||
"image": "localhost/fc-mysql-web:v20260617-sec4-storage-6fc3739",
|
||||
"image": "localhost/fc-mysql-web:v20260618-hm4-tenant-84dc65c",
|
||||
"imagePullPolicy": "Never",
|
||||
"livenessProbe": {
|
||||
"failureThreshold": 3,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"data": {
|
||||
"appsettings.Production.json": "{\"PhpManager\":{\"Namespace\":\"fc-php\",\"Slowlog\":{\"Path\":\"/var/log/apache2/php-fpm-slow.log\",\"Sidecar\":{\"Enabled\":true,\"Image\":\"\"}},\"PoolConfig\":{\"StartServers\":null,\"MinSpareServers\":null,\"MaxSpareServers\":null,\"ProcessIdleTimeoutSeconds\":10,\"RequestTerminateTimeoutSeconds\":30},\"Certificates\":{\"TlsInspector\":{\"LogGracefulDegradeWarnings\":false}},\"Backups\":{\"StoragePath\":\"/data/backups\"},\"Ingress\":{\"DefaultMiddlewares\":[{\"Name\":\"php-tenant-rate-limit\",\"Namespace\":\"fc-php\"},{\"Name\":\"php-tenant-secure-headers\",\"Namespace\":\"fc-php\"}],\"TlsOption\":{\"Name\":\"php-tenant-tls13\",\"Namespace\":\"fc-php\"}}},\"ApplicationArchives\":{\"WordPressCoreUrl\":\"http://php-web.fc-php.svc.cluster.local.:5400/api/v1/application-archives/wordpress/latest.tar.gz\",\"WordPressProxySourceUrl\":\"https://wordpress.org/latest.tar.gz\",\"WordPressLocalArchivePath\":\"/data/application-archives/latest.tar.gz\",\"MyBbCoreUrl\":\"http://php-web.fc-php.svc.cluster.local.:5400/api/v1/application-archives/mybb/latest.zip\",\"MyBbProxySourceUrl\":\"https://mybb.com/download/\",\"MyBbLocalArchivePath\":\"/data/application-archives/mybb-latest.zip\",\"MediaWikiCoreUrl\":\"http://php-web.fc-php.svc.cluster.local.:5400/api/v1/application-archives/mediawiki/latest.tar.gz\",\"MediaWikiProxySourceUrl\":\"https://releases.wikimedia.org/mediawiki/1.45/mediawiki-1.45.3.tar.gz\",\"MediaWikiLocalArchivePath\":\"/data/application-archives/mediawiki-latest.tar.gz\",\"DrupalCoreUrl\":\"http://php-web.fc-php.svc.cluster.local.:5400/api/v1/application-archives/drupal/latest.tar.gz\",\"DrupalProxySourceUrl\":\"https://ftp.drupal.org/files/projects/drupal-11.3.8.tar.gz\",\"DrupalLocalArchivePath\":\"/data/application-archives/drupal-latest.tar.gz\",\"BypassUpstreamTls\":true},\"ContainerBackend\":{\"Default\":\"Kubernetes\"},\"FlowerCore\":{\"Auth\":{\"Provider\":\"Oidc\",\"Enabled\":false,\"Oidc\":{\"Enabled\":true,\"Authority\":\"https://id.iamworkin.lan/application/o/php/\",\"Audience\":\"php\",\"ClientId\":\"php\",\"ClientSecret\":\"\"},\"Impersonation\":{\"Enabled\":false,\"DebugMode\":false}},\"Tenant\":{\"StrictMode\":false,\"JwtClaimsEnabled\":false,\"TenantClaimType\":\"fc:tenant\",\"ActorIdClaimType\":\"flowercore_actor_id\"},\"Account\":{\"AppId\":\"php\",\"DefaultTenantId\":\"default\",\"Impersonation\":{\"Enabled\":false,\"StrictMode\":false,\"TechSupportRoles\":[\"tech-support\"],\"Targets\":[]}},\"Hosting\":{\"AutoDns\":{\"Enabled\":true,\"DnsManagerBaseUrl\":\"https://dns.iamworkin.lan/\",\"ZoneName\":\"iamworkin.lan\",\"RecordType\":\"A\",\"TargetAddress\":\"10.0.56.200\",\"Ttl\":300,\"BypassTls\":true}},\"Database\":{\"Provider\":\"Sqlite\",\"ConnectionStrings\":{\"Sqlite\":\"Data Source=/data/php-manager.db\"}}}}"
|
||||
"appsettings.Production.json": "{\"PhpManager\":{\"Namespace\":\"fc-php\",\"Slowlog\":{\"Path\":\"/var/log/apache2/php-fpm-slow.log\",\"Sidecar\":{\"Enabled\":true,\"Image\":\"\"}},\"PoolConfig\":{\"StartServers\":null,\"MinSpareServers\":null,\"MaxSpareServers\":null,\"ProcessIdleTimeoutSeconds\":10,\"RequestTerminateTimeoutSeconds\":30},\"Certificates\":{\"TlsInspector\":{\"LogGracefulDegradeWarnings\":false}},\"Backups\":{\"StoragePath\":\"/data/backups\"},\"Ingress\":{\"DefaultMiddlewares\":[{\"Name\":\"php-tenant-rate-limit\",\"Namespace\":\"fc-php\"},{\"Name\":\"php-tenant-secure-headers\",\"Namespace\":\"fc-php\"}],\"TlsOption\":{\"Name\":\"php-tenant-tls13\",\"Namespace\":\"fc-php\"},\"Waf\":{\"Enabled\":true,\"Image\":\"owasp/modsecurity-crs:4.25-nginx-alpine-lts@sha256:88b59911549723e71beabf3b4aa47bbd31b00e79401f442e65ddfc430ae46343\",\"AllowedMethods\":\"GET HEAD POST OPTIONS DELETE\"}}},\"ApplicationArchives\":{\"WordPressCoreUrl\":\"http://php-web.fc-php.svc.cluster.local.:5400/api/v1/application-archives/wordpress/latest.tar.gz\",\"WordPressProxySourceUrl\":\"https://wordpress.org/latest.tar.gz\",\"WordPressLocalArchivePath\":\"/data/application-archives/latest.tar.gz\",\"MyBbCoreUrl\":\"http://php-web.fc-php.svc.cluster.local.:5400/api/v1/application-archives/mybb/latest.zip\",\"MyBbProxySourceUrl\":\"https://mybb.com/download/\",\"MyBbLocalArchivePath\":\"/data/application-archives/mybb-latest.zip\",\"MediaWikiCoreUrl\":\"http://php-web.fc-php.svc.cluster.local.:5400/api/v1/application-archives/mediawiki/latest.tar.gz\",\"MediaWikiProxySourceUrl\":\"https://releases.wikimedia.org/mediawiki/1.45/mediawiki-1.45.3.tar.gz\",\"MediaWikiLocalArchivePath\":\"/data/application-archives/mediawiki-latest.tar.gz\",\"DrupalCoreUrl\":\"http://php-web.fc-php.svc.cluster.local.:5400/api/v1/application-archives/drupal/latest.tar.gz\",\"DrupalProxySourceUrl\":\"https://ftp.drupal.org/files/projects/drupal-11.3.8.tar.gz\",\"DrupalLocalArchivePath\":\"/data/application-archives/drupal-latest.tar.gz\",\"BypassUpstreamTls\":true},\"ContainerBackend\":{\"Default\":\"Kubernetes\"},\"FlowerCore\":{\"Auth\":{\"Provider\":\"Oidc\",\"Enabled\":false,\"Oidc\":{\"Enabled\":true,\"Authority\":\"https://id.iamworkin.lan/application/o/php/\",\"Audience\":\"php\",\"ClientId\":\"php\",\"ClientSecret\":\"\"},\"Impersonation\":{\"Enabled\":false,\"DebugMode\":false}},\"Tenant\":{\"StrictMode\":false,\"JwtClaimsEnabled\":false,\"TenantClaimType\":\"fc:tenant\",\"ActorIdClaimType\":\"flowercore_actor_id\"},\"Account\":{\"AppId\":\"php\",\"DefaultTenantId\":\"default\",\"Impersonation\":{\"Enabled\":false,\"StrictMode\":false,\"TechSupportRoles\":[\"tech-support\"],\"Targets\":[]}},\"Hosting\":{\"AutoDns\":{\"Enabled\":true,\"DnsManagerBaseUrl\":\"https://dns.iamworkin.lan/\",\"ZoneName\":\"iamworkin.lan\",\"RecordType\":\"A\",\"TargetAddress\":\"10.0.57.202\",\"Ttl\":300,\"BypassTls\":true}},\"Database\":{\"Provider\":\"Sqlite\",\"ConnectionStrings\":{\"Sqlite\":\"Data Source=/data/php-manager.db\"}}}}"
|
||||
},
|
||||
"kind": "ConfigMap",
|
||||
"metadata": {
|
||||
|
||||
@@ -67,6 +67,10 @@
|
||||
"name": "MODSEC_AUDIT_LOG_TYPE",
|
||||
"value": "Serial"
|
||||
},
|
||||
{
|
||||
"name": "ALLOWED_METHODS",
|
||||
"value": "GET HEAD POST OPTIONS DELETE"
|
||||
},
|
||||
{
|
||||
"name": "LOGLEVEL",
|
||||
"value": "warn"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"template": {
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/restartedAt": "2026-06-13T01:59:27-05:00",
|
||||
"kubectl.kubernetes.io/restartedAt": "2026-06-19T00:00:00-05:00",
|
||||
"prometheus.io/path": "/metrics/prometheus",
|
||||
"prometheus.io/port": "5400",
|
||||
"prometheus.io/scrape": "true"
|
||||
@@ -86,7 +86,7 @@
|
||||
"value": "php"
|
||||
}
|
||||
],
|
||||
"image": "localhost/fc-php-web:v20260617-whc4-edge-638b3b3",
|
||||
"image": "localhost/fc-php-web:v20260619-whc4-generated-waf-147f02a",
|
||||
"imagePullPolicy": "Never",
|
||||
"livenessProbe": {
|
||||
"failureThreshold": 3,
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"image": "localhost/fc-kiosk-web:gx10-v1",
|
||||
"image": "localhost/fc-kiosk-web:v20260619-kiadmin-7cc83fd",
|
||||
"imagePullPolicy": "Never",
|
||||
"livenessProbe": {
|
||||
"failureThreshold": 6,
|
||||
@@ -195,7 +195,7 @@
|
||||
"-c",
|
||||
"mkdir -p /profiles/data && chown -R 1654:1654 /profiles/data && chmod -R u+rwX,g+rwX /profiles/data"
|
||||
],
|
||||
"image": "localhost/fc-kiosk-web:gx10-v1",
|
||||
"image": "localhost/fc-kiosk-web:v20260619-kiadmin-7cc83fd",
|
||||
"imagePullPolicy": "Never",
|
||||
"name": "fix-profile-perms",
|
||||
"resources": {},
|
||||
|
||||
@@ -66,11 +66,11 @@
|
||||
},
|
||||
{
|
||||
"name": "PhpManager__BaseUrl",
|
||||
"value": "https://php.iamworkin.lan/"
|
||||
"value": "http://php-web.fc-php.svc.cluster.local:5400/"
|
||||
},
|
||||
{
|
||||
"name": "PhpManager__BypassTls",
|
||||
"value": "true"
|
||||
"value": "false"
|
||||
}
|
||||
],
|
||||
"image": "localhost/fc-php-operator:v20260617-sec5-0bfbf42",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"data": {
|
||||
"default.conf": "server {\n listen 80;\n server_name _;\n root /usr/share/nginx/html;\n index index.html;\n location / { try_files $uri $uri/ =404; }\n location /healthz { access_log off; return 200 \"ok\"; add_header Content-Type text/plain; }\n}\n"
|
||||
},
|
||||
"default.conf": "server {\n listen 80;\n server_name _;\n root /usr/share/nginx/html;\n index index.html;\n location / { try_files $uri $uri/ =404; }\n location = /lamp-canary/index.php { add_header Content-Type text/plain; return 200 \"lamp-index-ok\\n\"; }\n location = /lamp-canary/wp-login.php { add_header Content-Type text/plain; return 200 \"wp-login-ok\\n\"; }\n location = /lamp-canary/mediawiki/index.php { add_header Content-Type text/plain; return 200 \"mediawiki-ok\\n\"; }\n location = /admin-allowlist-proof { add_header Content-Type text/plain; return 200 \"admin-allowlist-ok\\n\"; }\n location /healthz { access_log off; return 200 \"ok\"; add_header Content-Type text/plain; }\n}\n"
|
||||
},
|
||||
"kind": "ConfigMap",
|
||||
"metadata": {
|
||||
"name": "andrew-web-nginx-conf",
|
||||
|
||||
180
apps-gx10/fc-tenant-andrew/deployment-andrew-web-waf.json
Normal file
180
apps-gx10/fc-tenant-andrew/deployment-andrew-web-waf.json
Normal file
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app.kubernetes.io/managed-by": "flowercore",
|
||||
"app.kubernetes.io/name": "andrew-web-waf"
|
||||
},
|
||||
"name": "andrew-web-waf",
|
||||
"namespace": "fc-tenant-andrew"
|
||||
},
|
||||
"spec": {
|
||||
"progressDeadlineSeconds": 600,
|
||||
"replicas": 1,
|
||||
"revisionHistoryLimit": 10,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app.kubernetes.io/name": "andrew-web-waf"
|
||||
}
|
||||
},
|
||||
"strategy": {
|
||||
"type": "Recreate"
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app.kubernetes.io/name": "andrew-web-waf"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"env": [
|
||||
{
|
||||
"name": "BACKEND",
|
||||
"value": "http://andrew-web.fc-tenant-andrew.svc.cluster.local:80"
|
||||
},
|
||||
{
|
||||
"name": "SERVER_NAME",
|
||||
"value": "bluejay.dev www.bluejay.dev"
|
||||
},
|
||||
{
|
||||
"name": "PORT",
|
||||
"value": "8080"
|
||||
},
|
||||
{
|
||||
"name": "PROXY_PRESERVE_HOST",
|
||||
"value": "on"
|
||||
},
|
||||
{
|
||||
"name": "PROXY_TIMEOUT",
|
||||
"value": "60s"
|
||||
},
|
||||
{
|
||||
"name": "MODSEC_RULE_ENGINE",
|
||||
"value": "On"
|
||||
},
|
||||
{
|
||||
"name": "MODSEC_AUDIT_ENGINE",
|
||||
"value": "RelevantOnly"
|
||||
},
|
||||
{
|
||||
"name": "MODSEC_AUDIT_LOG",
|
||||
"value": "/dev/stdout"
|
||||
},
|
||||
{
|
||||
"name": "MODSEC_AUDIT_LOG_TYPE",
|
||||
"value": "Serial"
|
||||
},
|
||||
{
|
||||
"name": "LOGLEVEL",
|
||||
"value": "warn"
|
||||
},
|
||||
{
|
||||
"name": "ERRORLOG",
|
||||
"value": "/dev/stderr"
|
||||
},
|
||||
{
|
||||
"name": "ACCESSLOG",
|
||||
"value": "/dev/stdout"
|
||||
},
|
||||
{
|
||||
"name": "BLOCKING_PARANOIA",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"name": "DETECTION_PARANOIA",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"name": "ANOMALY_INBOUND",
|
||||
"value": "5"
|
||||
},
|
||||
{
|
||||
"name": "ANOMALY_OUTBOUND",
|
||||
"value": "4"
|
||||
}
|
||||
],
|
||||
"image": "owasp/modsecurity-crs:4.25-nginx-alpine-lts@sha256:88b59911549723e71beabf3b4aa47bbd31b00e79401f442e65ddfc430ae46343",
|
||||
"imagePullPolicy": "IfNotPresent",
|
||||
"livenessProbe": {
|
||||
"failureThreshold": 3,
|
||||
"httpGet": {
|
||||
"httpHeaders": [
|
||||
{
|
||||
"name": "Host",
|
||||
"value": "bluejay.dev"
|
||||
}
|
||||
],
|
||||
"path": "/healthz",
|
||||
"port": 8080,
|
||||
"scheme": "HTTP"
|
||||
},
|
||||
"initialDelaySeconds": 20,
|
||||
"periodSeconds": 30,
|
||||
"successThreshold": 1,
|
||||
"timeoutSeconds": 2
|
||||
},
|
||||
"name": "andrew-web-waf",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 8080,
|
||||
"name": "http",
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"readinessProbe": {
|
||||
"failureThreshold": 3,
|
||||
"httpGet": {
|
||||
"httpHeaders": [
|
||||
{
|
||||
"name": "Host",
|
||||
"value": "bluejay.dev"
|
||||
}
|
||||
],
|
||||
"path": "/healthz",
|
||||
"port": 8080,
|
||||
"scheme": "HTTP"
|
||||
},
|
||||
"initialDelaySeconds": 10,
|
||||
"periodSeconds": 10,
|
||||
"successThreshold": 1,
|
||||
"timeoutSeconds": 2
|
||||
},
|
||||
"resources": {
|
||||
"limits": {
|
||||
"cpu": "500m",
|
||||
"memory": "512Mi"
|
||||
},
|
||||
"requests": {
|
||||
"cpu": "100m",
|
||||
"memory": "128Mi"
|
||||
}
|
||||
},
|
||||
"securityContext": {
|
||||
"allowPrivilegeEscalation": false,
|
||||
"capabilities": {
|
||||
"drop": [
|
||||
"ALL"
|
||||
]
|
||||
}
|
||||
},
|
||||
"terminationMessagePath": "/dev/termination-log",
|
||||
"terminationMessagePolicy": "File"
|
||||
}
|
||||
],
|
||||
"enableServiceLinks": false,
|
||||
"restartPolicy": "Always",
|
||||
"schedulerName": "default-scheduler",
|
||||
"securityContext": {
|
||||
"fsGroup": 101,
|
||||
"runAsGroup": 101,
|
||||
"runAsNonRoot": true,
|
||||
"runAsUser": 101
|
||||
},
|
||||
"terminationGracePeriodSeconds": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,15 @@
|
||||
},
|
||||
"type": "RollingUpdate"
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "andrew-web"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"flowercore.io/config-revision": "whc4-lamp-allowlist-20260618"
|
||||
},
|
||||
"labels": {
|
||||
"app": "andrew-web"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
|
||||
@@ -11,19 +11,58 @@
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"kind": "Rule",
|
||||
"match": "Host(`bluejay.dev`) || Host(`www.bluejay.dev`)",
|
||||
"priority": 100,
|
||||
"services": [
|
||||
{
|
||||
"name": "andrew-web",
|
||||
"port": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tls": {
|
||||
"secretName": "cf-origin-bluejay-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
"kind": "Rule",
|
||||
"match": "Host(`bluejay.dev`) || Host(`www.bluejay.dev`)",
|
||||
"middlewares": [
|
||||
{
|
||||
"name": "andrew-tenant-rate-limit",
|
||||
"namespace": "fc-tenant-andrew"
|
||||
},
|
||||
{
|
||||
"name": "andrew-tenant-secure-headers",
|
||||
"namespace": "fc-tenant-andrew"
|
||||
}
|
||||
],
|
||||
"priority": 100,
|
||||
"services": [
|
||||
{
|
||||
"name": "andrew-web-waf",
|
||||
"port": 8080
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Rule",
|
||||
"match": "(Host(`bluejay.dev`) || Host(`www.bluejay.dev`)) && PathPrefix(`/admin-allowlist-proof`)",
|
||||
"middlewares": [
|
||||
{
|
||||
"name": "andrew-admin-ip-allowlist",
|
||||
"namespace": "fc-tenant-andrew"
|
||||
},
|
||||
{
|
||||
"name": "andrew-tenant-rate-limit",
|
||||
"namespace": "fc-tenant-andrew"
|
||||
},
|
||||
{
|
||||
"name": "andrew-tenant-secure-headers",
|
||||
"namespace": "fc-tenant-andrew"
|
||||
}
|
||||
],
|
||||
"priority": 300,
|
||||
"services": [
|
||||
{
|
||||
"name": "andrew-web-waf",
|
||||
"port": 8080
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tls": {
|
||||
"options": {
|
||||
"name": "andrew-tenant-tls13",
|
||||
"namespace": "fc-tenant-andrew"
|
||||
},
|
||||
"secretName": "cf-origin-bluejay-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"apiVersion": "traefik.io/v1alpha1",
|
||||
"kind": "Middleware",
|
||||
"metadata": {
|
||||
"name": "andrew-admin-ip-allowlist",
|
||||
"namespace": "fc-tenant-andrew"
|
||||
},
|
||||
"spec": {
|
||||
"ipAllowList": {
|
||||
"sourceRange": [
|
||||
"10.0.56.14/32"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"apiVersion": "traefik.io/v1alpha1",
|
||||
"kind": "Middleware",
|
||||
"metadata": {
|
||||
"name": "andrew-tenant-rate-limit",
|
||||
"namespace": "fc-tenant-andrew"
|
||||
},
|
||||
"spec": {
|
||||
"rateLimit": {
|
||||
"average": 120,
|
||||
"burst": 240,
|
||||
"period": "1m"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"apiVersion": "traefik.io/v1alpha1",
|
||||
"kind": "Middleware",
|
||||
"metadata": {
|
||||
"name": "andrew-tenant-secure-headers",
|
||||
"namespace": "fc-tenant-andrew"
|
||||
},
|
||||
"spec": {
|
||||
"headers": {
|
||||
"contentTypeNosniff": true,
|
||||
"browserXssFilter": true,
|
||||
"referrerPolicy": "strict-origin-when-cross-origin",
|
||||
"stsSeconds": 31536000,
|
||||
"stsIncludeSubdomains": true,
|
||||
"stsPreload": false
|
||||
}
|
||||
}
|
||||
}
|
||||
24
apps-gx10/fc-tenant-andrew/service-andrew-web-waf.json
Normal file
24
apps-gx10/fc-tenant-andrew/service-andrew-web-waf.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": {
|
||||
"name": "andrew-web-waf",
|
||||
"namespace": "fc-tenant-andrew"
|
||||
},
|
||||
"spec": {
|
||||
"internalTrafficPolicy": "Cluster",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 8080,
|
||||
"protocol": "TCP",
|
||||
"targetPort": 8080
|
||||
}
|
||||
],
|
||||
"selector": {
|
||||
"app.kubernetes.io/name": "andrew-web-waf"
|
||||
},
|
||||
"sessionAffinity": "None",
|
||||
"type": "ClusterIP"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"apiVersion": "traefik.io/v1alpha1",
|
||||
"kind": "TLSOption",
|
||||
"metadata": {
|
||||
"name": "andrew-tenant-tls13",
|
||||
"namespace": "fc-tenant-andrew"
|
||||
},
|
||||
"spec": {
|
||||
"minVersion": "VersionTLS13"
|
||||
}
|
||||
}
|
||||
11
apps-gx10/fc-tenant-default/namespace-fc-tenant-default.json
Normal file
11
apps-gx10/fc-tenant-default/namespace-fc-tenant-default.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Namespace",
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app.kubernetes.io/managed-by": "flowercore",
|
||||
"flowercore.io/tenant": "default"
|
||||
},
|
||||
"name": "fc-tenant-default"
|
||||
}
|
||||
}
|
||||
@@ -53,13 +53,17 @@
|
||||
"name": "FlowerCore__Updater__BundleStorage__LocalFs__RootDirectory",
|
||||
"value": "/data/bundles"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__Updater__PublicShares__RequirePublicVisibilityOnPublicHosts",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__Updater__PublicShares__Links__0__Code",
|
||||
"value": "8f3c2a9e7d41"
|
||||
{
|
||||
"name": "FlowerCore__Updater__PublicShares__RequirePublicVisibilityOnPublicHosts",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__Updater__PublicShares__RequireShareLinkOnPublicHosts",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__Updater__PublicShares__Links__0__Code",
|
||||
"value": "8f3c2a9e7d41"
|
||||
},
|
||||
{
|
||||
"name": "FlowerCore__Updater__PublicShares__Links__0__AppId",
|
||||
@@ -195,7 +199,7 @@
|
||||
"value": "26843545600"
|
||||
}
|
||||
],
|
||||
"image": "localhost/fc-updater-web:v20260617-sec5-913c6a9",
|
||||
"image": "localhost/fc-updater-web:v20260618-feed-signed-9cc9942",
|
||||
"imagePullPolicy": "Never",
|
||||
"securityContext": {
|
||||
"allowPrivilegeEscalation": false,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"routes": [
|
||||
{
|
||||
"kind": "Rule",
|
||||
"match": "(Host(`update.flowercore.io`) || Host(`updates.flowercore.io`)) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`))",
|
||||
"match": "(Host(`update.flowercore.io`) || Host(`updates.flowercore.io`)) && (Method(`GET`) || Method(`HEAD`))",
|
||||
"priority": 100,
|
||||
"services": [
|
||||
{
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
"value": "/data/vector-stores/corpus-cache"
|
||||
}
|
||||
],
|
||||
"image": "localhost/fc-knowledge-web:gx10-v1",
|
||||
"image": "localhost/fc-knowledge-web:v20260619-sec3-6370c95",
|
||||
"imagePullPolicy": "Never",
|
||||
"livenessProbe": {
|
||||
"failureThreshold": 3,
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
# Phase 1 production uses a Longhorn RWO PVC at /data/devicemgmt.db. The
|
||||
# 1Password runtime item stays mounted through env for future MySQL/API-key
|
||||
# cutover, but MySQL is not required for this first product-host rollout.
|
||||
# Image v20260613-g2-66a43c1 is built from FlowerCore.DeviceManagement master
|
||||
# 66a43c1, carrying edge enrollment network completion and SQLite-safe trust-bundle smoke coverage.
|
||||
# Image v20260618-prune-18c7449-livebase is derived from the 2026-06-17 AN-13
|
||||
# live base with the Mac fleet SQLite snapshot-prune hotfix from
|
||||
# FlowerCore.DeviceManagement PR #49.
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
@@ -83,7 +84,7 @@ spec:
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: web
|
||||
image: localhost/fc-devicemgmt-web:v20260614-regroup-c5b8f82
|
||||
image: localhost/fc-devicemgmt-web:v20260618-prune-18c7449-livebase
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- name: http
|
||||
|
||||
@@ -43,5 +43,6 @@ shared origin cert must exist in every namespace that serves a
|
||||
```powershell
|
||||
kubectl.exe --kubeconfig C:\Users\AndrewStoltz\.kube\rke2.yaml -n argocd get application infra-fc-updater
|
||||
kubectl.exe --kubeconfig C:\Users\AndrewStoltz\.kube\rke2.yaml -n fc-updater get deploy,svc,ingressroute,certificate,pvc
|
||||
curl.exe -sk https://update.flowercore.io/api/v1/manifests/_schema
|
||||
curl.exe -sk https://update.flowercore.io/
|
||||
curl.exe -sk -o NUL -w "%{http_code}`n" https://update.flowercore.io/login
|
||||
```
|
||||
|
||||
@@ -61,7 +61,7 @@ spec:
|
||||
nodeName: rke2-server
|
||||
containers:
|
||||
- name: web
|
||||
image: localhost/fc-updater-web:v20260614-regroup-bdf4a4a
|
||||
image: localhost/fc-updater-web:v20260618-feed-signed-9cc9942
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
@@ -266,7 +266,7 @@ spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: (Host(`update.flowercore.io`) || Host(`updates.flowercore.io`)) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`))
|
||||
- match: (Host(`update.flowercore.io`) || Host(`updates.flowercore.io`)) && (Method(`GET`) || Method(`HEAD`))
|
||||
kind: Rule
|
||||
services:
|
||||
- name: updatecenter-web
|
||||
|
||||
@@ -8,7 +8,8 @@ auto-deploy them there. Once ArgoCD is stood up on the GX10, a GX10-only
|
||||
ApplicationSet (`apps-gx10/*`) will own these.
|
||||
|
||||
- `step-ca-acme.yaml` — cert-manager ClusterIssuer (ACME → noc1 step-ca, in-spec caBundle). APPLIED + Ready.
|
||||
- `traefik-helmchart.yaml` — Traefik v3.6.10 (chart 39.0.5) via the RKE2 HelmChart CRD, LoadBalancer VIP 10.0.57.202 (prod-pool; temp parallel-run VIP — canonical .200 reclaimed at cutover). APPLIED.
|
||||
- `traefik-helmchart.yaml` — Traefik v3.6.10 (chart 39.0.5) via the RKE2 HelmChart CRD, LoadBalancer VIP 10.0.57.202 (prod-pool; temp parallel-run VIP — canonical .200 reclaimed at cutover), with `externalTrafficPolicy: Local` so tenant IP allowlists see client source IP instead of the GX10 node hop. APPLIED.
|
||||
- `gitea-ssh-service.yaml` — Gitea SSH LoadBalancer service on `10.0.57.206:22` with `externalTrafficPolicy: Local`; HTTPS Gitea remains behind the Traefik VIP at `10.0.57.202`. APPLIED.
|
||||
|
||||
cert-manager v1.17.2 was installed separately (upstream static manifest). See
|
||||
`docs/ai-agents/gx10-migration-continuation-2026-06-14.md` + memory
|
||||
|
||||
17
gx10/platform/gitea-ssh-service.yaml
Normal file
17
gx10/platform/gitea-ssh-service.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gitea-ssh
|
||||
namespace: gitea
|
||||
annotations:
|
||||
metallb.io/loadBalancerIPs: 10.0.57.206
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
externalTrafficPolicy: Local
|
||||
selector:
|
||||
app: gitea
|
||||
ports:
|
||||
- name: ssh
|
||||
port: 22
|
||||
protocol: TCP
|
||||
targetPort: 2222
|
||||
@@ -10,72 +10,27 @@ spec:
|
||||
targetNamespace: traefik-system
|
||||
createNamespace: true
|
||||
valuesContent: |
|
||||
deployment:
|
||||
replicas: 1
|
||||
additionalArguments:
|
||||
- "--api.dashboard=true"
|
||||
- "--log.level=INFO"
|
||||
- "--providers.kubernetescrd"
|
||||
- "--providers.kubernetesingress"
|
||||
- "--providers.kubernetescrd.allowEmptyServices=true"
|
||||
- "--providers.kubernetesingress.allowEmptyServices=true"
|
||||
- "--providers.kubernetesingress.ingressendpoint.publishedservice=traefik-system/traefik"
|
||||
service:
|
||||
type: LoadBalancer
|
||||
spec:
|
||||
externalTrafficPolicy: Local
|
||||
annotations:
|
||||
metallb.io/loadBalancerIPs: 10.0.57.202
|
||||
ingressClass:
|
||||
enabled: true
|
||||
isDefaultClass: false
|
||||
providers:
|
||||
kubernetesCRD:
|
||||
enabled: true
|
||||
allowEmptyServices: true
|
||||
kubernetesIngress:
|
||||
enabled: true
|
||||
allowEmptyServices: true
|
||||
publishedService:
|
||||
enabled: true
|
||||
ingressRoute:
|
||||
dashboard:
|
||||
enabled: false
|
||||
rbac:
|
||||
enabled: true
|
||||
service:
|
||||
type: LoadBalancer
|
||||
annotations:
|
||||
metallb.io/loadBalancerIPs: "10.0.57.202"
|
||||
metallb.io/address-pool: "prod-pool"
|
||||
ports:
|
||||
web:
|
||||
port: 8000
|
||||
exposedPort: 80
|
||||
protocol: TCP
|
||||
websecure:
|
||||
port: 8443
|
||||
exposedPort: 443
|
||||
protocol: TCP
|
||||
tls:
|
||||
enabled: true
|
||||
irc:
|
||||
port: 6667
|
||||
exposedPort: 6667
|
||||
protocol: TCP
|
||||
expose:
|
||||
default: true
|
||||
irctls:
|
||||
port: 6697
|
||||
exposedPort: 6697
|
||||
protocol: TCP
|
||||
expose:
|
||||
default: true
|
||||
traefik:
|
||||
port: 8080
|
||||
exposedPort: 8080
|
||||
protocol: TCP
|
||||
expose:
|
||||
default: false
|
||||
metrics:
|
||||
port: 9100
|
||||
exposedPort: 9100
|
||||
protocol: TCP
|
||||
expose:
|
||||
default: false
|
||||
metrics:
|
||||
prometheus:
|
||||
entryPoint: metrics
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "256Mi"
|
||||
tolerations:
|
||||
- key: "node-role.kubernetes.io/control-plane"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
logs:
|
||||
general:
|
||||
level: INFO
|
||||
|
||||
@@ -16,6 +16,8 @@ public sealed class FleetManifestLintTests
|
||||
{
|
||||
"brochure.flowercore.io",
|
||||
"dist.flowercore.io",
|
||||
"update.flowercore.io",
|
||||
"updates.flowercore.io",
|
||||
};
|
||||
|
||||
// Hosts that allow a tightly bounded write surface in addition to GET/HEAD.
|
||||
@@ -247,6 +249,22 @@ public sealed class FleetManifestLintTests
|
||||
violations.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gx10PublicLoadBalancers_MustPreserveClientSourceIp()
|
||||
{
|
||||
var traefikPath = Path.Combine(Inventory.BluejayRoot, "gx10", "platform", "traefik-helmchart.yaml");
|
||||
var traefik = File.ReadAllText(traefikPath);
|
||||
|
||||
traefik.Should().Contain("metallb.io/loadBalancerIPs: 10.0.57.202");
|
||||
traefik.Should().Contain("spec:\n externalTrafficPolicy: Local");
|
||||
|
||||
var giteaPath = Path.Combine(Inventory.BluejayRoot, "gx10", "platform", "gitea-ssh-service.yaml");
|
||||
var gitea = File.ReadAllText(giteaPath);
|
||||
|
||||
gitea.Should().Contain("metallb.io/loadBalancerIPs: 10.0.57.206");
|
||||
gitea.Should().Contain("externalTrafficPolicy: Local");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApiKeyProtectedDeployments_MustUseTcpSocketHealthProbes()
|
||||
{
|
||||
@@ -981,6 +999,26 @@ public sealed class FleetManifestLintTests
|
||||
gatewayManifest.Should().Contain("port: 5400");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gx10DeviceManagementWriteApis_RequireRuntimeBackedOperatorAuth()
|
||||
{
|
||||
var web = Gx10DeploymentContainer("fc-devicemgmt", "deployment-fc-devicemgmt-web.json");
|
||||
|
||||
JsonEnvValue(web, "FlowerCore__Auth__Enabled").Should().Be("true");
|
||||
JsonEnvSecretName(web, "Auth__ApiKey").Should().Be("fc-devicemgmt-runtime");
|
||||
JsonEnvSecretKey(web, "Auth__ApiKey").Should().Be("DEVICE_MANAGEMENT_OPERATOR_API_KEY");
|
||||
JsonEnvSecretOptional(web, "Auth__ApiKey").Should().BeNull();
|
||||
JsonEnvSecretName(web, "FlowerCore__Auth__ApiKey").Should().Be("fc-devicemgmt-runtime");
|
||||
JsonEnvSecretKey(web, "FlowerCore__Auth__ApiKey").Should().Be("DEVICE_MANAGEMENT_OPERATOR_API_KEY");
|
||||
JsonEnvSecretOptional(web, "FlowerCore__Auth__ApiKey").Should().BeNull();
|
||||
JsonEnvSecretName(web, "Auth__AdminApiKey").Should().Be("fc-devicemgmt-runtime");
|
||||
JsonEnvSecretKey(web, "Auth__AdminApiKey").Should().Be("DEVICE_MANAGEMENT_ADMIN_API_KEY");
|
||||
JsonEnvSecretOptional(web, "Auth__AdminApiKey").Should().BeNull();
|
||||
JsonEnvSecretName(web, "FlowerCore__Auth__AdminApiKey").Should().Be("fc-devicemgmt-runtime");
|
||||
JsonEnvSecretKey(web, "FlowerCore__Auth__AdminApiKey").Should().Be("DEVICE_MANAGEMENT_ADMIN_API_KEY");
|
||||
JsonEnvSecretOptional(web, "FlowerCore__Auth__AdminApiKey").Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gx10PhpTenantRoutes_HaveEdgeControlSubstrate()
|
||||
{
|
||||
@@ -1071,6 +1109,120 @@ public sealed class FleetManifestLintTests
|
||||
serviceRef.GetProperty("port").GetInt32().Should().Be(8080);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gx10BluejayDevTenantRoute_IsFrontedByOwaspCrsWaf()
|
||||
{
|
||||
var appRoot = Path.Combine(Inventory.BluejayRoot, "apps-gx10", "fc-tenant-andrew");
|
||||
var wafContainer = Gx10DeploymentContainer("fc-tenant-andrew", "deployment-andrew-web-waf.json");
|
||||
wafContainer.GetProperty("image").GetString()
|
||||
.Should()
|
||||
.Be("owasp/modsecurity-crs:4.25-nginx-alpine-lts@sha256:88b59911549723e71beabf3b4aa47bbd31b00e79401f442e65ddfc430ae46343");
|
||||
JsonEnvValue(wafContainer, "BACKEND").Should().Be("http://andrew-web.fc-tenant-andrew.svc.cluster.local:80");
|
||||
JsonEnvValue(wafContainer, "SERVER_NAME").Should().Be("bluejay.dev www.bluejay.dev");
|
||||
JsonEnvValue(wafContainer, "MODSEC_RULE_ENGINE").Should().Be("On");
|
||||
JsonEnvValue(wafContainer, "MODSEC_AUDIT_ENGINE").Should().Be("RelevantOnly");
|
||||
JsonEnvValue(wafContainer, "MODSEC_AUDIT_LOG").Should().Be("/dev/stdout");
|
||||
|
||||
using var wafDeployment = JsonDocument.Parse(File.ReadAllText(Path.Combine(appRoot, "deployment-andrew-web-waf.json")));
|
||||
var podSpec = wafDeployment.RootElement
|
||||
.GetProperty("spec")
|
||||
.GetProperty("template")
|
||||
.GetProperty("spec");
|
||||
podSpec.GetProperty("enableServiceLinks").GetBoolean().Should().BeFalse();
|
||||
podSpec.GetProperty("securityContext").GetProperty("runAsUser").GetInt32().Should().Be(101);
|
||||
podSpec.GetProperty("securityContext").GetProperty("runAsNonRoot").GetBoolean().Should().BeTrue();
|
||||
wafContainer.GetProperty("readinessProbe")
|
||||
.GetProperty("httpGet")
|
||||
.GetProperty("httpHeaders")[0]
|
||||
.GetProperty("value")
|
||||
.GetString()
|
||||
.Should()
|
||||
.Be("bluejay.dev");
|
||||
|
||||
using var service = JsonDocument.Parse(File.ReadAllText(Path.Combine(appRoot, "service-andrew-web-waf.json")));
|
||||
service.RootElement.GetProperty("spec").GetProperty("selector").GetProperty("app.kubernetes.io/name").GetString().Should().Be("andrew-web-waf");
|
||||
var servicePort = service.RootElement.GetProperty("spec").GetProperty("ports").EnumerateArray().Should().ContainSingle().Subject;
|
||||
servicePort.GetProperty("port").GetInt32().Should().Be(8080);
|
||||
servicePort.GetProperty("targetPort").GetInt32().Should().Be(8080);
|
||||
|
||||
using var ingressRoute = JsonDocument.Parse(File.ReadAllText(Path.Combine(appRoot, "ingressroute-andrew-web.json")));
|
||||
var route = ingressRoute.RootElement
|
||||
.GetProperty("spec")
|
||||
.GetProperty("routes")[0];
|
||||
var serviceRef = route
|
||||
.GetProperty("services")
|
||||
.EnumerateArray()
|
||||
.Should()
|
||||
.ContainSingle()
|
||||
.Subject;
|
||||
serviceRef.GetProperty("name").GetString().Should().Be("andrew-web-waf");
|
||||
serviceRef.GetProperty("port").GetInt32().Should().Be(8080);
|
||||
|
||||
route.GetProperty("middlewares")
|
||||
.EnumerateArray()
|
||||
.Select(item => item.GetProperty("name").GetString())
|
||||
.Should()
|
||||
.Equal("andrew-tenant-rate-limit", "andrew-tenant-secure-headers");
|
||||
|
||||
var adminRoute = ingressRoute.RootElement
|
||||
.GetProperty("spec")
|
||||
.GetProperty("routes")
|
||||
.EnumerateArray()
|
||||
.Single(route => route.GetProperty("match").GetString()!.Contains("PathPrefix(`/admin-allowlist-proof`)", StringComparison.Ordinal));
|
||||
adminRoute.GetProperty("priority").GetInt32().Should().Be(300);
|
||||
adminRoute.GetProperty("services").EnumerateArray().Should().ContainSingle().Subject
|
||||
.GetProperty("name").GetString().Should().Be("andrew-web-waf");
|
||||
adminRoute.GetProperty("middlewares")
|
||||
.EnumerateArray()
|
||||
.Select(item => item.GetProperty("name").GetString())
|
||||
.Should()
|
||||
.Equal("andrew-admin-ip-allowlist", "andrew-tenant-rate-limit", "andrew-tenant-secure-headers");
|
||||
|
||||
using var rateLimit = JsonDocument.Parse(File.ReadAllText(Path.Combine(appRoot, "middleware-andrew-tenant-rate-limit.json")));
|
||||
rateLimit.RootElement.GetProperty("spec").GetProperty("rateLimit").GetProperty("average").GetInt32().Should().Be(120);
|
||||
|
||||
using var allowlist = JsonDocument.Parse(File.ReadAllText(Path.Combine(appRoot, "middleware-andrew-admin-ip-allowlist.json")));
|
||||
allowlist.RootElement.GetProperty("kind").GetString().Should().Be("Middleware");
|
||||
allowlist.RootElement.GetProperty("spec").GetProperty("ipAllowList").GetProperty("sourceRange")
|
||||
.EnumerateArray()
|
||||
.Select(item => item.GetString())
|
||||
.Should()
|
||||
.Equal("10.0.56.14/32");
|
||||
|
||||
using var nginxConfig = JsonDocument.Parse(File.ReadAllText(Path.Combine(appRoot, "configmap-andrew-web-nginx-conf.json")));
|
||||
var nginx = nginxConfig.RootElement.GetProperty("data").GetProperty("default.conf").GetString();
|
||||
nginx.Should().Contain("location = /lamp-canary/index.php");
|
||||
nginx.Should().Contain("location = /lamp-canary/wp-login.php");
|
||||
nginx.Should().Contain("location = /lamp-canary/mediawiki/index.php");
|
||||
nginx.Should().Contain("location = /admin-allowlist-proof");
|
||||
|
||||
using var webDeployment = JsonDocument.Parse(File.ReadAllText(Path.Combine(appRoot, "deployment-andrew-web.json")));
|
||||
webDeployment.RootElement.GetProperty("spec")
|
||||
.GetProperty("template")
|
||||
.GetProperty("metadata")
|
||||
.GetProperty("annotations")
|
||||
.GetProperty("flowercore.io/config-revision")
|
||||
.GetString()
|
||||
.Should()
|
||||
.Be("whc4-lamp-allowlist-20260618");
|
||||
|
||||
using var headers = JsonDocument.Parse(File.ReadAllText(Path.Combine(appRoot, "middleware-andrew-tenant-secure-headers.json")));
|
||||
var headerSpec = headers.RootElement.GetProperty("spec").GetProperty("headers");
|
||||
headerSpec.GetProperty("contentTypeNosniff").GetBoolean().Should().BeTrue();
|
||||
headerSpec.GetProperty("stsSeconds").GetInt32().Should().Be(31536000);
|
||||
|
||||
using var tlsOption = JsonDocument.Parse(File.ReadAllText(Path.Combine(appRoot, "tlsoption-andrew-tenant-tls13.json")));
|
||||
tlsOption.RootElement.GetProperty("spec").GetProperty("minVersion").GetString().Should().Be("VersionTLS13");
|
||||
ingressRoute.RootElement
|
||||
.GetProperty("spec")
|
||||
.GetProperty("tls")
|
||||
.GetProperty("options")
|
||||
.GetProperty("name")
|
||||
.GetString()
|
||||
.Should()
|
||||
.Be("andrew-tenant-tls13");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gx10HostingManagers_ProvisioningCrdsAndRbacMustBeGitOpsOwned()
|
||||
{
|
||||
@@ -1172,6 +1324,39 @@ public sealed class FleetManifestLintTests
|
||||
match.Should().NotContain("Method(`POST`)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateCenterPublicIngress_KeepsDeliveryOnlyGetHeadMethodAllowlist()
|
||||
{
|
||||
var publicIngress = AppDocuments("fc-updater")
|
||||
.Single(document => document.Kind == "IngressRoute" && document.Name == "updatecenter-web-public");
|
||||
var route = publicIngress.MappingSequence("spec", "routes").Should().ContainSingle().Subject;
|
||||
var match = ManifestNodeExtensions.Scalar(route, "match");
|
||||
|
||||
match.Should().Contain("Host(`update.flowercore.io`)");
|
||||
match.Should().Contain("Host(`updates.flowercore.io`)");
|
||||
match.Should().Contain("Method(`GET`)");
|
||||
match.Should().Contain("Method(`HEAD`)");
|
||||
match.Should().NotContain("Method(`POST`)");
|
||||
match.Should().NotContain("Method(`OPTIONS`)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gx10UpdateCenterPublicIngress_StaysGetHeadOnlyAndUsesContainmentImage()
|
||||
{
|
||||
var appRoot = Path.Combine(Inventory.BluejayRoot, "apps-gx10", "fc-updater");
|
||||
var publicRoute = File.ReadAllText(Path.Combine(appRoot, "ingressroute-updatecenter-web-public-gx10.json"));
|
||||
var deployment = File.ReadAllText(Path.Combine(appRoot, "deployment-updatecenter-web.json"));
|
||||
|
||||
publicRoute.Should().Contain("Host(`update.flowercore.io`)");
|
||||
publicRoute.Should().Contain("Host(`updates.flowercore.io`)");
|
||||
publicRoute.Should().Contain("Method(`GET`)");
|
||||
publicRoute.Should().Contain("Method(`HEAD`)");
|
||||
publicRoute.Should().NotContain("Method(`POST`)");
|
||||
publicRoute.Should().NotContain("Method(`OPTIONS`)");
|
||||
deployment.Should().Contain("localhost/fc-updater-web:v");
|
||||
deployment.Should().NotContain("localhost/fc-updater-web:v20260614-regroup-bdf4a4a");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DnsAndMediaIngressRoutes_MatchLiveInternalHosts()
|
||||
{
|
||||
@@ -1283,9 +1468,13 @@ public sealed class FleetManifestLintTests
|
||||
|
||||
private static bool? JsonEnvSecretOptional(JsonElement container, string name)
|
||||
{
|
||||
return JsonEnvMapping(container, name) is { } env
|
||||
? env.GetProperty("valueFrom").GetProperty("secretKeyRef").GetProperty("optional").GetBoolean()
|
||||
: null;
|
||||
if (JsonEnvMapping(container, name) is not { } env)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var secretKeyRef = env.GetProperty("valueFrom").GetProperty("secretKeyRef");
|
||||
return secretKeyRef.TryGetProperty("optional", out var optional) ? optional.GetBoolean() : null;
|
||||
}
|
||||
|
||||
private static string? JsonEnvValue(JsonElement container, string name)
|
||||
|
||||
208
tests/bluejay-infra-lint/Gx10AppleMdmNanohubTests.cs
Normal file
208
tests/bluejay-infra-lint/Gx10AppleMdmNanohubTests.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace BluejayInfraLint.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class Gx10AppleMdmNanohubTests
|
||||
{
|
||||
private static readonly string Root = FindRepoRoot();
|
||||
private static readonly string AppRoot = Path.Combine(Root, "apps-gx10", "fc-apple-mdm");
|
||||
private static readonly IReadOnlyList<YamlMappingNode> Documents = LoadDocuments();
|
||||
|
||||
[Fact]
|
||||
public void Manifest_DeclaresLockedDownNanoHubRuntime()
|
||||
{
|
||||
Documents.Should().Contain(document => Is(document, "Namespace", "fc-apple-mdm"));
|
||||
Documents.Should().Contain(document => Is(document, "ConfigMap", "fc-apple-mdm-root-ca"));
|
||||
Documents.Should().Contain(document => Is(document, "Service", "fc-apple-mdm"));
|
||||
Documents.Should().Contain(document => Is(document, "Service", "fc-apple-mdm-scep"));
|
||||
Documents.Should().Contain(document => Is(document, "EndpointSlice", "fc-apple-mdm-scep-noc1"));
|
||||
Documents.Should().Contain(document => Is(document, "NetworkPolicy", "fc-apple-mdm-netpol"));
|
||||
Documents.Should().NotContain(document => (document.Scalar("kind") ?? string.Empty) == "Secret");
|
||||
Documents.Should().NotContain(document => (document.Scalar("kind") ?? string.Empty) == "OnePasswordItem");
|
||||
|
||||
var pvc = Single("PersistentVolumeClaim", "fc-apple-mdm-data");
|
||||
pvc.Scalar("spec", "storageClassName").Should().Be("local-path");
|
||||
pvc.Scalar("spec", "resources", "requests", "storage").Should().Be("2Gi");
|
||||
|
||||
var deployment = Single("Deployment", "fc-apple-mdm");
|
||||
deployment.Scalar("spec", "strategy", "type").Should().Be("Recreate");
|
||||
deployment.Scalar("spec", "template", "metadata", "annotations", "fc.flowercore.io/probe-path").Should().Be("/version");
|
||||
deployment.Scalar("spec", "template", "metadata", "annotations", "flowercore.io/root-ca-sha256")
|
||||
.Should()
|
||||
.Be("a9120c88fa3ec735d790aa4cfeb61ac2946730338969015bebaccc08fe10535e");
|
||||
|
||||
var maybePodSpec = deployment.Mapping("spec", "template", "spec");
|
||||
maybePodSpec.Should().NotBeNull();
|
||||
var podSpec = maybePodSpec!;
|
||||
podSpec.Scalar("enableServiceLinks").Should().Be("false");
|
||||
podSpec.Scalar("securityContext", "runAsUser").Should().Be("1654");
|
||||
podSpec.Scalar("securityContext", "runAsNonRoot").Should().Be("true");
|
||||
|
||||
var container = podSpec.MappingSequence("containers").Should().ContainSingle().Subject;
|
||||
container.Scalar("name").Should().Be("nanohub");
|
||||
container.Scalar("image").Should().Be("localhost/fc-apple-mdm-nanohub:v0.2.0-20260617");
|
||||
container.Scalar("imagePullPolicy").Should().Be("Never");
|
||||
container.Scalar("securityContext", "readOnlyRootFilesystem").Should().Be("true");
|
||||
container.Scalar("securityContext", "allowPrivilegeEscalation").Should().Be("false");
|
||||
|
||||
EnvValue(container, "NANOHUB_LISTEN").Should().Be(":9004");
|
||||
EnvValue(container, "NANOHUB_STORAGE").Should().Be("file");
|
||||
EnvValue(container, "NANOHUB_STORAGE_DSN").Should().Be("/var/lib/nanohub/db");
|
||||
EnvValue(container, "NANOHUB_CHECKIN").Should().Be("true");
|
||||
EnvValue(container, "NANOHUB_CA").Should().Be("/etc/nanohub/ca/root_ca.crt");
|
||||
EnvSecretName(container, "NANOHUB_API_KEY").Should().Be("fc-apple-mdm-runtime");
|
||||
EnvSecretKey(container, "NANOHUB_API_KEY").Should().Be("NANOHUB_API_KEY");
|
||||
EnvSecretName(container, "NANOHUB_WEBHOOK_URL").Should().Be("fc-apple-mdm-runtime");
|
||||
EnvSecretKey(container, "NANOHUB_WEBHOOK_URL").Should().Be("NANOHUB_WEBHOOK_URL");
|
||||
EnvSecretOptional(container, "NANOHUB_WEBHOOK_URL").Should().Be("true");
|
||||
|
||||
VolumeMount(container, "data").Scalar("mountPath").Should().Be("/var/lib/nanohub");
|
||||
VolumeMount(container, "root-ca").Scalar("mountPath").Should().Be("/etc/nanohub/ca");
|
||||
VolumeMount(container, "root-ca").Scalar("readOnly").Should().Be("true");
|
||||
ProbePath(container, "startupProbe").Should().Be("/version");
|
||||
ProbePath(container, "readinessProbe").Should().Be("/version");
|
||||
container.Scalar("livenessProbe", "tcpSocket", "port").Should().Be("9004");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Manifest_ExposesOnlyMdmCheckinVersionAndScepPaths()
|
||||
{
|
||||
var certificate = Single("Certificate", "fc-apple-mdm-tls");
|
||||
certificate.Scalar("spec", "issuerRef", "name").Should().Be("step-ca-acme");
|
||||
certificate.Scalar("spec", "issuerRef", "kind").Should().Be("ClusterIssuer");
|
||||
certificate.ScalarSequence("spec", "dnsNames").Should().ContainSingle("mdm.iamworkin.lan");
|
||||
|
||||
var scepService = Single("Service", "fc-apple-mdm-scep");
|
||||
scepService.Scalar("spec", "type").Should().Be("ClusterIP");
|
||||
var scepServicePort = scepService.MappingSequence("spec", "ports").Should().ContainSingle().Subject;
|
||||
scepServicePort.Scalar("name").Should().Be("http");
|
||||
scepServicePort.Scalar("port").Should().Be("80");
|
||||
scepServicePort.Scalar("targetPort").Should().Be("9080");
|
||||
|
||||
var scepEndpointSlice = Single("EndpointSlice", "fc-apple-mdm-scep-noc1");
|
||||
scepEndpointSlice.Scalar("addressType").Should().Be("IPv4");
|
||||
scepEndpointSlice.Scalar("metadata", "labels", "kubernetes.io/service-name").Should().Be("fc-apple-mdm-scep");
|
||||
var scepEndpoint = scepEndpointSlice.MappingSequence("endpoints").Should().ContainSingle().Subject;
|
||||
scepEndpoint.ScalarSequence("addresses").Should().ContainSingle("10.0.56.10");
|
||||
var scepEndpointPort = scepEndpointSlice.MappingSequence("ports").Should().ContainSingle().Subject;
|
||||
scepEndpointPort.Scalar("name").Should().Be("http");
|
||||
scepEndpointPort.Scalar("port").Should().Be("9080");
|
||||
|
||||
var ingress = Single("IngressRoute", "fc-apple-mdm");
|
||||
var routes = ingress.MappingSequence("spec", "routes");
|
||||
routes.Should().HaveCount(2);
|
||||
var scepRoute = routes.Single(route => route.Scalar("match")?.Contains("PathPrefix(`/scep`)") == true);
|
||||
var nanohubRoute = routes.Single(route => route.Scalar("match")?.Contains("PathPrefix(`/mdm`)") == true);
|
||||
var match = nanohubRoute.Scalar("match");
|
||||
|
||||
match.Should().Contain("Host(`mdm.iamworkin.lan`)");
|
||||
match.Should().Contain("PathPrefix(`/mdm`)");
|
||||
match.Should().Contain("PathPrefix(`/checkin`)");
|
||||
match.Should().Contain("PathPrefix(`/version`)");
|
||||
match.Should().NotContain("/api/v1");
|
||||
match.Should().NotContain("PathPrefix(`/api`)");
|
||||
|
||||
scepRoute.Scalar("match").Should().Contain("Host(`mdm.iamworkin.lan`)");
|
||||
scepRoute.Scalar("match").Should().Contain("PathPrefix(`/scep`)");
|
||||
var scepRouteService = scepRoute.MappingSequence("services").Should().ContainSingle().Subject;
|
||||
scepRouteService.Scalar("name").Should().Be("fc-apple-mdm-scep");
|
||||
scepRouteService.Scalar("port").Should().Be("80");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Readme_DocumentsSecretImportAndSupportBoundary()
|
||||
{
|
||||
var readme = File.ReadAllText(Path.Combine(AppRoot, "README.md"));
|
||||
|
||||
readme.Should().Contain("FlowerCore Apple MDM Runtime");
|
||||
readme.Should().Contain("Secret/fc-apple-mdm-runtime");
|
||||
readme.Should().Contain("imagePullPolicy: Never");
|
||||
readme.Should().Contain("10.0.57.202");
|
||||
readme.Should().Contain("https://mdm.iamworkin.lan/scep/apple-mdm-scep");
|
||||
readme.Should().Contain("Smallstep SCEP requires an RSA intermediate");
|
||||
readme.Should().Contain("does not create an APNs MDM push certificate");
|
||||
readme.Should().Contain("managed Wi-Fi payload");
|
||||
}
|
||||
|
||||
private static YamlMappingNode Single(string kind, string name)
|
||||
{
|
||||
return Documents.Single(document => Is(document, kind, name));
|
||||
}
|
||||
|
||||
private static bool Is(YamlMappingNode document, string kind, string name)
|
||||
{
|
||||
return document.Scalar("kind") == kind
|
||||
&& document.Scalar("metadata", "name") == name;
|
||||
}
|
||||
|
||||
private static string? EnvValue(YamlMappingNode container, string name)
|
||||
{
|
||||
return EnvMapping(container, name)?.Scalar("value");
|
||||
}
|
||||
|
||||
private static string? EnvSecretName(YamlMappingNode container, string name)
|
||||
{
|
||||
return EnvMapping(container, name)?.Scalar("valueFrom", "secretKeyRef", "name");
|
||||
}
|
||||
|
||||
private static string? EnvSecretKey(YamlMappingNode container, string name)
|
||||
{
|
||||
return EnvMapping(container, name)?.Scalar("valueFrom", "secretKeyRef", "key");
|
||||
}
|
||||
|
||||
private static string? EnvSecretOptional(YamlMappingNode container, string name)
|
||||
{
|
||||
return EnvMapping(container, name)?.Scalar("valueFrom", "secretKeyRef", "optional");
|
||||
}
|
||||
|
||||
private static string? ProbePath(YamlMappingNode container, string probeKey)
|
||||
{
|
||||
return container.Scalar(probeKey, "httpGet", "path");
|
||||
}
|
||||
|
||||
private static YamlMappingNode VolumeMount(YamlMappingNode container, string name)
|
||||
{
|
||||
return container.MappingSequence("volumeMounts")
|
||||
.Single(mount => mount.Scalar("name") == name);
|
||||
}
|
||||
|
||||
private static YamlMappingNode? EnvMapping(YamlMappingNode container, string name)
|
||||
{
|
||||
return container.MappingSequence("env")
|
||||
.SingleOrDefault(env => env.Scalar("name") == name);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<YamlMappingNode> LoadDocuments()
|
||||
{
|
||||
var stream = new YamlStream();
|
||||
using var reader = File.OpenText(Path.Combine(AppRoot, "fc-apple-mdm.yaml"));
|
||||
stream.Load(reader);
|
||||
|
||||
return stream.Documents
|
||||
.Select(document => document.RootNode)
|
||||
.OfType<YamlMappingNode>()
|
||||
.Where(mapping => !string.IsNullOrWhiteSpace(mapping.Scalar("kind")))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(current.FullName, "apps-gx10"))
|
||||
&& Directory.Exists(Path.Combine(current.FullName, "tests"))
|
||||
&& File.Exists(Path.Combine(current.FullName, "README.md")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Could not find bluejay-infra root.");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
package bluejayinfra.public_method_allowlist
|
||||
|
||||
public_hosts := {"brochure.flowercore.io", "dist.flowercore.io", "dns.iamworkin.lan"}
|
||||
public_hosts := {
|
||||
"brochure.flowercore.io",
|
||||
"dist.flowercore.io",
|
||||
"dns.iamworkin.lan",
|
||||
"update.flowercore.io",
|
||||
"updates.flowercore.io",
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
input.kind == "IngressRoute"
|
||||
|
||||
@@ -9,8 +9,6 @@ package bluejayinfra.public_readwrite_allowlist
|
||||
public_readwrite_hosts := {
|
||||
"updatecenter.iamworkin.lan",
|
||||
"updates.iamworkin.lan",
|
||||
"update.flowercore.io",
|
||||
"updates.flowercore.io",
|
||||
}
|
||||
|
||||
required_methods := {"GET", "HEAD", "POST", "OPTIONS"}
|
||||
|
||||
Reference in New Issue
Block a user