apiVersion: apps/v1 kind: Deployment metadata: name: my-ui namespace: gsc-my labels: app: my-ui component: frontend spec: replicas: 2 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: my-ui template: metadata: labels: app: my-ui component: frontend # Egress to the in-cluster web-proxy (Squid) for NextAuth's # Keycloak issuer-discovery fetch to auth.gosec.cloud. egress-internet: "true" # Egress to the internal-gateway proxy (Envoy) so the app can # reach postgresql.internal-gateway.svc.cluster.local:5432. # Selected by the allow-internal-gateway GlobalNetworkPolicy. egress-internal: "true" spec: containers: - name: my-ui image: registry.gosec.internal/gsc-my/ui:v0.1.4 imagePullPolicy: Always ports: - containerPort: 3000 name: http env: - name: NODE_ENV value: "production" # Route Node fetch() through Squid for Keycloak discovery. - name: HTTP_PROXY value: "http://web-proxy.web-proxy.svc.cluster.local:3128" - name: HTTPS_PROXY value: "http://web-proxy.web-proxy.svc.cluster.local:3128" - name: NO_PROXY value: "localhost,127.0.0.1,.cluster.local,.svc,.gosec.internal" - name: NEXT_PUBLIC_APP_URL value: "https://my.gosec.internal" # Postgres (gsc_core — admin/nav/public/shell schemas). - name: PGHOST valueFrom: secretKeyRef: name: gsc-my-db key: host - name: PGPORT valueFrom: secretKeyRef: name: gsc-my-db key: port - name: PGUSER valueFrom: secretKeyRef: name: gsc-my-db key: user - name: PGPASSWORD valueFrom: secretKeyRef: name: gsc-my-db key: password - name: PGDATABASE valueFrom: secretKeyRef: name: gsc-my-db key: database - name: DATABASE_URL value: "postgresql://$(PGUSER):$(PGPASSWORD)@$(PGHOST):$(PGPORT)/$(PGDATABASE)" # NextAuth + Keycloak (gosecCloud realm). - name: AUTH_KEYCLOAK_ID valueFrom: secretKeyRef: name: my-ui key: keycloak-client-id - name: AUTH_KEYCLOAK_SECRET valueFrom: secretKeyRef: name: my-ui key: keycloak-client-secret - name: AUTH_KEYCLOAK_ISSUER value: "https://auth.gosec.cloud/realms/gosecCloud" - name: NEXTAUTH_URL value: "https://my.gosec.internal" # NextAuth v5 prefers AUTH_URL over NEXTAUTH_URL. Set both # so a stale .env baked into a future image build can't # shadow the runtime config and emit form actions pointing # at the wrong host. - name: AUTH_URL value: "https://my.gosec.internal" - name: AUTH_TRUST_HOST value: "true" - name: NEXTAUTH_SECRET valueFrom: secretKeyRef: name: my-ui key: nextauth-secret # gsc-ops-api (mTLS) for chat contacts route. Cert files are # mounted from a separate secret if/when the route is used; # leaving the URL unset disables the contacts provider # gracefully — see src/app/api/chat/contacts/route.ts. # - name: OPS_API_URL # - name: OPS_API_KEY resources: requests: memory: "384Mi" cpu: "100m" limits: memory: "768Mi" cpu: "500m" readinessProbe: httpGet: path: /api/health port: 3000 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 livenessProbe: httpGet: path: /api/health port: 3000 initialDelaySeconds: 30 periodSeconds: 30 timeoutSeconds: 5 securityContext: runAsNonRoot: true runAsUser: 1000 allowPrivilegeEscalation: false readOnlyRootFilesystem: true volumeMounts: - name: tmp mountPath: /tmp - name: nextjs-cache mountPath: /app/.next/cache volumes: - name: tmp emptyDir: {} - name: nextjs-cache emptyDir: {} imagePullSecrets: - name: registry-credentials affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - my-ui topologyKey: kubernetes.io/hostname --- apiVersion: v1 kind: Service metadata: name: my-ui namespace: gsc-my labels: app: my-ui spec: type: ClusterIP ports: - port: 3000 targetPort: 3000 protocol: TCP name: http selector: app: my-ui --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: my-ui namespace: gsc-my annotations: nginx.ingress.kubernetes.io/proxy-body-size: "10m" nginx.ingress.kubernetes.io/proxy-read-timeout: "60" nginx.ingress.kubernetes.io/proxy-send-timeout: "60" nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/force-ssl-redirect: "true" # ModSecurity + OWASP CRS is enabled cluster-wide; the chunked # NextAuth session cookie (>4 KB of base64 JWE) trips CRS rules # and returns 403 before the request reaches the pod. Same fix # applied to crm-ui / support-ui / chronos / gsc-meet / etc. nginx.ingress.kubernetes.io/enable-modsecurity: "false" nginx.ingress.kubernetes.io/proxy-buffer-size: "16k" nginx.ingress.kubernetes.io/proxy-buffers-number: "8" # Hardening headers (CSP comes from cluster ConfigMap). nginx.ingress.kubernetes.io/configuration-snippet: | more_set_headers "X-Frame-Options: SAMEORIGIN"; more_set_headers "X-Content-Type-Options: nosniff"; more_set_headers "Referrer-Policy: strict-origin-when-cross-origin"; more_set_headers "Strict-Transport-Security: max-age=31536000; includeSubDomains"; spec: ingressClassName: nginx tls: - hosts: - my.gosec.internal secretName: my-tls-internal rules: - host: my.gosec.internal http: paths: - path: / pathType: Prefix backend: service: name: my-ui port: number: 3000