--- # Namespace (trust zone; default-deny + policies apply) apiVersion: v1 kind: Namespace metadata: name: semaphore labels: homelab.keithjackson.dev/app-namespace: 'true' gateway-access: shared annotations: homelab.keithjackson.dev/backup-policy: daily-offcluster --- # Default-deny baseline (R-NET-01): deny all, allow only DNS apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny namespace: semaphore labels: homelab.keithjackson.dev/baseline: default-deny spec: podSelector: {} policyTypes: - Ingress - Egress ingress: [] egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system ports: - protocol: UDP port: 53 - protocol: TCP port: 53 --- # PVC semaphore-config (R-STATE) apiVersion: v1 kind: PersistentVolumeClaim metadata: name: semaphore-config namespace: semaphore labels: app.kubernetes.io/name: semaphore spec: accessModes: - ReadWriteOnce storageClassName: longhorn resources: requests: storage: 1Gi --- # Deployment (R-PLAT-02) apiVersion: apps/v1 kind: Deployment metadata: name: semaphore namespace: semaphore labels: app.kubernetes.io/name: semaphore spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app.kubernetes.io/name: semaphore template: metadata: labels: app.kubernetes.io/name: semaphore homelab.keithjackson.dev/egress-class: internet homelab.keithjackson.dev/auth-mode: oidc annotations: homelab.keithjackson.dev/auth-mode: oidc homelab.keithjackson.dev/auth-groups: platform-admins spec: containers: - name: semaphore image: semaphoreui/semaphore:v2.19.17 ports: - containerPort: 3000 env: - name: SEMAPHORE_DB_DIALECT value: postgres - name: SEMAPHORE_DB_HOST value: semaphore-db-rw.semaphore-db.svc.cluster.local - name: SEMAPHORE_DB_PORT value: '5432' - name: SEMAPHORE_DB value: semaphore - name: SEMAPHORE_DB_USER value: semaphore - name: SEMAPHORE_DB_PASS valueFrom: secretKeyRef: name: semaphore-bootstrap key: db-password - name: SEMAPHORE_ADMIN valueFrom: secretKeyRef: name: semaphore-bootstrap key: admin-username - name: SEMAPHORE_ADMIN_PASSWORD valueFrom: secretKeyRef: name: semaphore-bootstrap key: admin-password - name: SEMAPHORE_ADMIN_NAME value: Semaphore Admin - name: SEMAPHORE_ADMIN_EMAIL value: admin@${BASE_DOMAIN} - name: SEMAPHORE_ACCESS_KEY_ENCRYPTION valueFrom: secretKeyRef: name: semaphore-bootstrap key: access-key-encryption - name: SEMAPHORE_PORT value: :3000 - name: SEMAPHORE_OIDC_CLIENT_SECRET valueFrom: secretKeyRef: name: semaphore-bootstrap key: oidc-client-secret - name: SEMAPHORE_OIDC_PROVIDERS value: '{"keycloak":{"display_name":"Keycloak","provider_url":"https://keycloak.${BASE_DOMAIN}/realms/homelab","client_id":"semaphore","client_secret":"$(SEMAPHORE_OIDC_CLIENT_SECRET)","redirect_url":"https://semaphore.${BASE_DOMAIN}/api/auth/oidc/keycloak/redirect","scopes":["openid","profile","email"],"username_claim":"preferred_username","email_claim":"email","name_claim":"name","order":1}}' volumeMounts: - name: config mountPath: /etc/semaphore securityContext: runAsUser: 1001 runAsGroup: 0 runAsNonRoot: true fsGroup: 1001 volumes: - name: config persistentVolumeClaim: claimName: semaphore-config --- # Service apiVersion: v1 kind: Service metadata: name: semaphore namespace: semaphore labels: app.kubernetes.io/name: semaphore spec: selector: app.kubernetes.io/name: semaphore ports: - name: port-3000 port: 3000 targetPort: 3000 --- # CiliumNetworkPolicy egress allow (R-NET-02): semaphore-allow-semaphore-db-5432 apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: semaphore-allow-semaphore-db-5432 namespace: semaphore labels: app.kubernetes.io/name: semaphore spec: endpointSelector: matchLabels: app.kubernetes.io/name: semaphore egress: - toEndpoints: - matchLabels: app.kubernetes.io/name: semaphore-db k8s:io.kubernetes.pod.namespace: semaphore-db toPorts: - ports: - port: '5432' protocol: TCP --- # CiliumNetworkPolicy gateway-ingress allow (R-NET-01/02): semaphore-allow-gateway-ingress apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: semaphore-allow-gateway-ingress namespace: semaphore labels: app.kubernetes.io/name: semaphore spec: endpointSelector: matchLabels: app.kubernetes.io/name: semaphore ingress: - fromEntities: - ingress toPorts: - ports: - port: '3000' protocol: TCP --- # CiliumNetworkPolicy world egress allow (egress class governs the path; R-NET-05): semaphore-allow-internet-egress apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: semaphore-allow-internet-egress namespace: semaphore labels: app.kubernetes.io/name: semaphore spec: endpointSelector: matchLabels: app.kubernetes.io/name: semaphore egress: - toEntities: - world - toEndpoints: - matchLabels: io.kubernetes.pod.namespace: keycloak app: keycloak --- # HTTPRoute semaphore-3000 (R-TLS/R-DNS) apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: semaphore-3000 namespace: semaphore labels: app.kubernetes.io/name: semaphore annotations: homelab.keithjackson.dev/auth-mode: oidc homelab.keithjackson.dev/auth-groups: platform-admins external-dns.alpha.kubernetes.io/controller: none spec: parentRefs: - name: shared namespace: gateway sectionName: https hostnames: - semaphore.${BASE_DOMAIN} rules: - matches: - path: type: PathPrefix value: / backendRefs: - name: semaphore port: 3000 --- # PrometheusRule SLI alerts (R-OBS-01) apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: name: semaphore-slis namespace: semaphore labels: release: kube-prometheus-stack app.kubernetes.io/name: semaphore spec: groups: - name: semaphore.slis rules: - alert: semaphore-down expr: kube_deployment_status_replicas_available{namespace="semaphore", deployment="semaphore"} == 0 or absent(kube_deployment_status_replicas_available{namespace="semaphore", deployment="semaphore"}) for: 5m labels: severity: critical service: semaphore annotations: summary: semaphore is down description: 'semaphore: its Deployment has no available/ready replica for 5m, or the signal is gone entirely (SLI: up, via kube_deployment_status_replicas_available).'