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:
223
k8s/deployment.yaml
Normal file
223
k8s/deployment.yaml
Normal 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
|
||||
Reference in New Issue
Block a user