chore: bootstrap gscMy on @gsc/web-kit + PAM/JIT request flow

Initial commit for gscMy carved out as its own repo (was tracked
loosely under the monorepo's web/ which is gitignored).

What this contains:
- Auth: next-auth v5 via @gsc/web-kit createAuth (Keycloak only,
  identity sourced from claims, no admin.users writes)
- Chrome: @gsc/web-kit AdminShell — replaces the legacy MyShell.
  Sidebar JSON config carried over and mapped to DbMenuItem.
- Middleware: createAuthMiddleware. Public: /access-denied,
  /auth/keycloak, /signed-out, /api/health, /api/pam/approve.
- RP-initiated signout at /api/auth/signout → Keycloak end_session →
  /signed-out (mirrors gscAdmin).
- Phosphor-iconned access-denied + signed-out landing pages.

PAM/JIT request flow (ported from gscAdmin's pre-strip git history):
- /access page (Active + Eligible tables, request modal with
  duration slider + justification + optional MFA)
- API: /api/pam/{eligible, active, audit, request, approve/:token,
  revoke/:id}
- src/lib/{authz, pam, pam-mail, pam-mfa}.ts — same files as
  gscAdmin had before the strip. PAM tables (admin.privilege_*)
  are shared with gscAdmin; gscMy uses the same Prisma model defs.
- Top-bar widget shows active grants with countdown + revoke.

Build/Deploy: Dockerfile (monorepo-root context), k8s manifests for
my.gosec.internal, self-signed TLS placeholder, DNS A record.
Keycloak gsc-my client extended to include my.gosec.internal/* in
redirect_uris + web_origins.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Super User
2026-05-18 13:46:13 +02:00
commit be1c4fe5f9
96 changed files with 49849 additions and 0 deletions

223
k8s/deployment.yaml Normal file
View File

@@ -0,0 +1,223 @@
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.0
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

7
k8s/namespace.yaml Normal file
View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: gsc-my
labels:
app.kubernetes.io/name: gsc-my
istio.io/dataplane-mode: ambient