The kit's session.user.id is a NextAuth UUID — opaque, per-session
plumbing. The canonical cross-service identity is the FreeIPA uid
exposed as `gscSid` in the kit session. Grants written by gscMy
must use that key so gscAdmin's authz lookup (which uses the
same gscSID-as-key convention) hits them.
- All /api/pam/* routes now require `user.gscSid` instead of `user.id`
- authz.hasRole/requireRole/hasAnyRole take `{gscSid,roles}` shape
- whoami debug endpoint now shows gscSid for verification
- New helper src/lib/session-helpers.ts:getGscsid() for callers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
224 lines
7.1 KiB
YAML
224 lines
7.1 KiB
YAML
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.3
|
|
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
|