--- # Namespace (trust zone; default-deny + policies apply) apiVersion: v1 kind: Namespace metadata: name: plex 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: plex 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 --- # PersistentVolume plex-media - static external NFS (bulk tier) apiVersion: v1 kind: PersistentVolume metadata: name: plex-media labels: app.kubernetes.io/name: plex spec: capacity: storage: 9Ti accessModes: - ReadOnlyMany persistentVolumeReclaimPolicy: Retain storageClassName: '' mountOptions: - nfsvers=4.1 - hard - noatime - timeo=600 - retrans=2 nfs: server: ${BULK_NFS_SERVER} path: /mnt/Storage/plex readOnly: true claimRef: namespace: plex name: plex-media --- # PVC plex-config (R-STATE) apiVersion: v1 kind: PersistentVolumeClaim metadata: name: plex-config namespace: plex labels: app.kubernetes.io/name: plex spec: accessModes: - ReadWriteOnce storageClassName: longhorn resources: requests: storage: 8Gi --- # PVC plex-media (R-STATE) apiVersion: v1 kind: PersistentVolumeClaim metadata: name: plex-media namespace: plex labels: app.kubernetes.io/name: plex spec: accessModes: - ReadOnlyMany storageClassName: '' volumeName: plex-media resources: requests: storage: 9Ti --- # Deployment (R-PLAT-02) apiVersion: apps/v1 kind: Deployment metadata: name: plex namespace: plex labels: app.kubernetes.io/name: plex spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app.kubernetes.io/name: plex template: metadata: labels: app.kubernetes.io/name: plex homelab.keithjackson.dev/egress-class: internet homelab.keithjackson.dev/auth-mode: native spec: containers: - name: plex image: plexinc/pms-docker:latest ports: - containerPort: 32400 env: - name: PLEX_UID value: '1000' - name: PLEX_GID value: '1000' - name: TZ value: Etc/UTC - name: ALLOWED_NETWORKS value: 10.0.0.0/8,192.168.0.0/16 volumeMounts: - name: config mountPath: /config - name: media mountPath: /media readOnly: true volumes: - name: config persistentVolumeClaim: claimName: plex-config - name: media persistentVolumeClaim: claimName: plex-media --- # Service apiVersion: v1 kind: Service metadata: name: plex namespace: plex labels: app.kubernetes.io/name: plex spec: selector: app.kubernetes.io/name: plex ports: - name: port-32400 port: 32400 targetPort: 32400 --- # CiliumNetworkPolicy reciprocal ingress allow (R-NET-02): plex-allow-seerr-32400 apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: plex-allow-seerr-32400 namespace: plex labels: app.kubernetes.io/name: plex spec: endpointSelector: matchLabels: app.kubernetes.io/name: plex ingress: - fromEndpoints: - matchLabels: app.kubernetes.io/name: seerr k8s:io.kubernetes.pod.namespace: seerr toPorts: - ports: - port: '32400' protocol: TCP --- # CiliumNetworkPolicy gateway-ingress allow (R-NET-01/02): plex-allow-gateway-ingress apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: plex-allow-gateway-ingress namespace: plex labels: app.kubernetes.io/name: plex spec: endpointSelector: matchLabels: app.kubernetes.io/name: plex ingress: - fromEntities: - ingress toPorts: - ports: - port: '32400' protocol: TCP --- # CiliumNetworkPolicy world egress allow (egress class governs the path; R-NET-05): plex-allow-internet-egress apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: plex-allow-internet-egress namespace: plex labels: app.kubernetes.io/name: plex spec: endpointSelector: matchLabels: app.kubernetes.io/name: plex egress: - toEntities: - world --- # HTTPRoute plex-32400 (R-TLS/R-DNS) apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: plex-32400 namespace: plex labels: app.kubernetes.io/name: plex spec: parentRefs: - name: shared namespace: gateway sectionName: https hostnames: - plex.${BASE_DOMAIN} rules: - matches: - path: type: PathPrefix value: / backendRefs: - name: plex port: 32400 --- # ServiceMonitor (R-OBS-01) apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: plex namespace: plex labels: release: kube-prometheus-stack app.kubernetes.io/name: plex spec: selector: matchLabels: app.kubernetes.io/name: plex namespaceSelector: matchNames: - plex endpoints: - port: metrics interval: 30s --- # PrometheusRule SLI alerts (R-OBS-01) apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: name: plex-slis namespace: plex labels: release: kube-prometheus-stack app.kubernetes.io/name: plex spec: groups: - name: plex.slis rules: - alert: plex-down expr: up{job="plex"} == 0 for: 5m labels: severity: critical service: plex annotations: summary: plex is down description: 'plex has been unreachable for 5m (SLI: up).'