mirror of
https://github.com/sredevopsorg/deepseek-kubernetes-sandbox.git
synced 2026-11-03 01:13:24 +00:00
feat(repo)!: repo reorganization (#16)
* feat(repo)!: restructure around the dsh-harness image and one example per implementation
- the root Dockerfile is the primary artifact; versions.env is the single source of truth
- merge gateway-api/dsh-demo into examples/dsh-agents-as-a-service (manifests/, exposure variants)
- move the single-tenant template into examples/minimal-single-tenant
- publish the gateway as a nested package under the repository image
- mirror the DSH ref for release identities (first release dsh-v0.2.1-rc.2-r2)
- CI: lint/python/manifests/gateway-image, reusable multi-arch build, tag-driven release, weekly e2e
- docs: README, docs/{main-image,examples,gitflow,versioning}, example READMEs, AGENTS.md, governance
- SSO is opt-in again; gateway pull policy and cookie defaults match the documented demo
BREAKING CHANGE: example manifest paths, the gateway image name, and the removal
of deploy/ and gateway-api/ affect existing deployments; see the migration table
in README.md.
* docs(changelog): generate initial changelog from history
* fix(scripts): make set-version update Dockerfile and sample revision pins
* build(hadolint): document intentional lint exceptions for the main image
---------
Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
This commit is contained in:
135
examples/dsh-agents-as-a-service/manifests/exposure/README.md
Normal file
135
examples/dsh-agents-as-a-service/manifests/exposure/README.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Exposing the gateway
|
||||
|
||||
The base manifests deploy the gateway as a ClusterIP Service
|
||||
(`dsh-gateway:8080`) and expose nothing outside the cluster. WebSocket
|
||||
upgrades on `/u/<user>/**` are part of the contract, so the entry point must
|
||||
forward `Upgrade`/`Connection` headers (Traefik does by default).
|
||||
|
||||
There are two **mutually exclusive** exposure variants. Pick exactly one:
|
||||
|
||||
| Variant | Entry point | Authentication | TLS |
|
||||
| --- | --- | --- | --- |
|
||||
| [`ingress/60-ingress.yaml`](ingress/60-ingress.yaml) | Traefik `Ingress` | the gateway's own signup/session cookie | optional, recommended |
|
||||
| [`gateway-api-sso/`](gateway-api-sso/72-oauth2-proxy.yaml) | Gateway API `Gateway` + `HTTPRoute` | GitHub OAuth via oauth2-proxy, then the gateway cookie | required (the Gateway listener terminates it) |
|
||||
|
||||
Applying both gives the gateway two entry points, and the SSO variant is only
|
||||
safe when the gateway is reachable **exclusively** through oauth2-proxy. Do not
|
||||
mix them.
|
||||
|
||||
## Applying a variant
|
||||
|
||||
Run these commands from the example root
|
||||
(`examples/dsh-agents-as-a-service/`), the same working directory the example
|
||||
README uses.
|
||||
|
||||
`kubectl apply -f manifests/` is **not recursive**: it applies only the
|
||||
top-level numbered files and does not descend into `exposure/`. Apply the base
|
||||
resources first, then the chosen variant explicitly:
|
||||
|
||||
```bash
|
||||
# Base resources: namespace, template, warm pool, claims, gateway.
|
||||
kubectl apply -f manifests/
|
||||
|
||||
# Plain Ingress exposure:
|
||||
kubectl apply -f manifests/exposure/ingress/
|
||||
|
||||
# ... or SSO exposure (create the two Secrets and replace the example.com
|
||||
# placeholders first — see below):
|
||||
kubectl apply -f manifests/exposure/gateway-api-sso/
|
||||
```
|
||||
|
||||
**Never use `kubectl apply -R`** (recursive): it would apply both variants at
|
||||
once and hand out a second, unauthenticated path to the same gateway. Apply the
|
||||
variant directory on purpose, or the individual files by name.
|
||||
|
||||
`kubectl` only reads `.yaml`, `.yml` and `.json` files from a directory, so
|
||||
the `*.yaml.example` secret templates are skipped automatically; create the real
|
||||
Secrets imperatively (commands below) before applying the directory.
|
||||
|
||||
## `ingress/`: plain Ingress, no SSO
|
||||
|
||||
`60-ingress.yaml` routes a host through the cluster's Traefik
|
||||
(`ingressClassName: traefik`; k3s ships one) to the gateway Service.
|
||||
|
||||
1. Replace the `dsh.example.com` host and the `dsh-example-tls` Secret name
|
||||
with real values, or delete the `tls:` block to serve plain HTTP.
|
||||
2. Set `DSH_TRUSTED_HOSTS` in the SandboxTemplate to the same public host:
|
||||
the gateway forwards the browser's `Host`, and DSH rejects unknown hosts on
|
||||
`/api` (see `../10-sandboxtemplate.yaml`).
|
||||
3. Set `COOKIE_SECURE=true` on the gateway once the Ingress serves HTTPS:
|
||||
`kubectl -n dsh-demo set env deploy/dsh-gateway COOKIE_SECURE=true`. The
|
||||
base manifest keeps `false` so the plain-HTTP port-forward demo works; a
|
||||
`Secure` cookie is never sent over HTTP, which silently breaks login.
|
||||
4. Do not attach buffering or compression middlewares to this router: they
|
||||
break WebSocket upgrades and streaming.
|
||||
|
||||
Verify: `curl -sI https://<host>/healthz` returns `200`.
|
||||
|
||||
## `gateway-api-sso/`: TLS + GitHub OAuth
|
||||
|
||||
`70-gateway.yaml` defines the TLS `Gateway`, `71-httproute.yaml` sends every
|
||||
request to oauth2-proxy, and `72-oauth2-proxy.yaml` authenticates the browser
|
||||
against GitHub and proxies to the gateway with `X-Forwarded-User` set.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Gateway API CRDs and a Gateway implementation installed;
|
||||
`gatewayClassName: traefik` matches the Traefik Gateway provider. The CRD
|
||||
version is pinned in `versions.env` (`GATEWAY_API_VERSION`).
|
||||
- The OAuth App credentials and a cookie secret, from
|
||||
`73-oauth2-proxy-secret.yaml.example`.
|
||||
- A TLS certificate/key for the public host, from
|
||||
`74-tls-secret.yaml.example`.
|
||||
- oauth2-proxy is pinned in the manifest to
|
||||
`quay.io/oauth2-proxy/oauth2-proxy:v7.15.5` (`versions.env`:
|
||||
`OAUTH2_PROXY_IMAGE`).
|
||||
|
||||
Steps:
|
||||
|
||||
```bash
|
||||
# 1. Real Secrets (the committed files are *.yaml.example templates).
|
||||
kubectl -n dsh-demo create secret generic oauth2-proxy \
|
||||
--from-literal=client-id='<GitHub OAuth App client ID>' \
|
||||
--from-literal=client-secret='<GitHub OAuth App client secret>' \
|
||||
--from-literal=cookie-secret="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
|
||||
kubectl -n dsh-demo create secret tls dsh-demo-tls --cert=tls.crt --key=tls.key
|
||||
|
||||
# 2. Replace dsh-demo.example.com (70/71/72) and your-github-org (72) with
|
||||
# the real public host and allowed GitHub organization.
|
||||
|
||||
# 3. Enable SSO on the gateway and trust the public host in the template.
|
||||
kubectl -n dsh-demo set env deploy/dsh-gateway \
|
||||
AUTH_PROXY_HEADER=X-Forwarded-User COOKIE_SECURE=true
|
||||
# then set DSH_TRUSTED_HOSTS=dsh-demo.example.com in the SandboxTemplate
|
||||
# (see ../10-sandboxtemplate.yaml) and re-apply it.
|
||||
|
||||
# 4. Apply the variant.
|
||||
kubectl apply -f manifests/exposure/gateway-api-sso/
|
||||
```
|
||||
|
||||
### SSO trust boundary
|
||||
|
||||
`AUTH_PROXY_HEADER` is opt-in and deliberately unset in the base gateway
|
||||
manifest: the gateway ignores identity headers until it is set. Enabling SSO
|
||||
takes **both** sides:
|
||||
|
||||
1. `AUTH_PROXY_HEADER=X-Forwarded-User` on `deploy/dsh-gateway` (step 3
|
||||
above). Without it, oauth2-proxy's login has no effect on the gateway and
|
||||
users are still asked for the gateway's own token.
|
||||
2. The gateway Service must be reachable **only through oauth2-proxy**: keep it
|
||||
ClusterIP, route all browser traffic through the `HTTPRoute` →
|
||||
oauth2-proxy → `dsh-gateway` chain, and never apply `ingress/` (or any
|
||||
other path to the Service) while SSO is enabled. A client that can reach
|
||||
`dsh-gateway:8080` directly can set `X-Forwarded-User` itself and
|
||||
impersonate any user.
|
||||
|
||||
Also terminate TLS in front of the gateway (the Gateway listener does) and
|
||||
leave `COOKIE_SECURE=true` so the session cookie never travels in clear text.
|
||||
|
||||
## Switching variants
|
||||
|
||||
Delete the variant you had before applying the other one, e.g.
|
||||
`kubectl delete -f manifests/exposure/ingress/`, and revert
|
||||
`AUTH_PROXY_HEADER`/`COOKIE_SECURE` on the gateway. Verify with
|
||||
`kubectl -n dsh-demo get ingress,gateway,httproute` that exactly one path to
|
||||
the gateway remains.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Browser entry point (TLS) for the gateway-api-sso variant: terminates TLS
|
||||
# for dsh-demo.example.com and routes through 71-httproute.yaml to
|
||||
# oauth2-proxy, which authenticates the browser before the gateway.
|
||||
# Mutually exclusive with ../../ingress/ (see ../../README.md). The
|
||||
# "dsh-demo-tls" Secret comes from 74-tls-secret.yaml.example.
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: dsh-gateway
|
||||
namespace: dsh-demo
|
||||
labels:
|
||||
app.kubernetes.io/part-of: dsh-demo
|
||||
spec:
|
||||
gatewayClassName: traefik
|
||||
listeners:
|
||||
- name: https
|
||||
port: 8443
|
||||
protocol: HTTPS
|
||||
tls:
|
||||
mode: Terminate
|
||||
certificateRefs:
|
||||
- name: dsh-demo-tls
|
||||
allowedRoutes:
|
||||
namespaces:
|
||||
from: Same
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: dsh-gateway
|
||||
namespace: dsh-demo
|
||||
labels:
|
||||
app.kubernetes.io/part-of: dsh-demo
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: dsh-gateway
|
||||
namespace: dsh-demo
|
||||
sectionName: https
|
||||
hostnames:
|
||||
# TODO: replace with the actual public FQDN before applying.
|
||||
- dsh-demo.example.com
|
||||
rules:
|
||||
- backendRefs:
|
||||
# Route through oauth2-proxy (GitHub OAuth) first; it authenticates the
|
||||
# browser and forwards to dsh-gateway:8080 with X-Forwarded-User set.
|
||||
# The gateway only trusts that header when AUTH_PROXY_HEADER is set on
|
||||
# its Deployment — see 72-oauth2-proxy.yaml and ../README.md.
|
||||
- name: oauth2-proxy
|
||||
port: 4180
|
||||
@@ -0,0 +1,134 @@
|
||||
# oauth2-proxy — GitHub OAuth login in front of the dsh-gateway.
|
||||
#
|
||||
# Runs as a reverse proxy: it authenticates the browser against GitHub
|
||||
# (restricted to a configured organization), then forwards the request to
|
||||
# the dsh-gateway Service with the authenticated identity in X-Forwarded-User
|
||||
# (plus X-Auth-Request-* via --set-xauthrequest). The gateway trusts that
|
||||
# header only when AUTH_PROXY_HEADER is set on its Deployment.
|
||||
#
|
||||
# ENABLING SSO TAKES BOTH SIDES (see ../../../exposure/README.md):
|
||||
# 1. Set AUTH_PROXY_HEADER=X-Forwarded-User on deploy/dsh-gateway, otherwise
|
||||
# the gateway ignores the header and the login is cosmetic:
|
||||
# kubectl -n dsh-demo set env deploy/dsh-gateway \
|
||||
# AUTH_PROXY_HEADER=X-Forwarded-User COOKIE_SECURE=true
|
||||
# 2. Make dsh-gateway reachable ONLY through this proxy: keep the Service
|
||||
# ClusterIP, route browser traffic through 71-httproute.yaml, and do not
|
||||
# apply ../ingress/ (or anything else that reaches the gateway). A
|
||||
# client that can reach dsh-gateway:8080 can spoof X-Forwarded-User and
|
||||
# impersonate any user.
|
||||
# TLS terminates at the Gateway listener (70-gateway.yaml); keep
|
||||
# --cookie-secure=true and COOKIE_SECURE=true on the gateway.
|
||||
#
|
||||
# The image is pinned to quay.io/oauth2-proxy/oauth2-proxy:v7.15.5
|
||||
# (versions.env: OAUTH2_PROXY_IMAGE). Credentials come from the
|
||||
# "oauth2-proxy" Secret (see 73-oauth2-proxy-secret.yaml.example); this
|
||||
# manifest never carries secrets.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: oauth2-proxy
|
||||
namespace: dsh-demo
|
||||
labels:
|
||||
app: oauth2-proxy
|
||||
app.kubernetes.io/part-of: dsh-demo
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: oauth2-proxy
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: oauth2-proxy
|
||||
app.kubernetes.io/part-of: dsh-demo
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 2000
|
||||
runAsGroup: 2000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: oauth2-proxy
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.5
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
args:
|
||||
- --http-address=0.0.0.0:4180
|
||||
- --provider=github
|
||||
# TODO: replace with the GitHub organization allowed to sign in.
|
||||
- --github-org=your-github-org
|
||||
- --email-domain=*
|
||||
# Reverse-proxy target: the gateway backend Service.
|
||||
- --upstream=http://dsh-gateway:8080
|
||||
# Public callback (TLS terminates at the Gateway listener).
|
||||
# TODO: replace with the actual public FQDN.
|
||||
- --redirect-url=https://dsh-demo.example.com/oauth2/callback
|
||||
- --cookie-secure=true
|
||||
- --cookie-name=_oauth2_proxy
|
||||
# Also expose X-Auth-Request-User/Email/Groups upstream.
|
||||
- --set-xauthrequest
|
||||
# Single provider: skip the provider-selection page.
|
||||
- --skip-provider-button
|
||||
env:
|
||||
- name: OAUTH2_PROXY_CLIENT_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: oauth2-proxy
|
||||
key: client-id
|
||||
- name: OAUTH2_PROXY_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: oauth2-proxy
|
||||
key: client-secret
|
||||
- name: OAUTH2_PROXY_COOKIE_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: oauth2-proxy
|
||||
key: cookie-secret
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 4180
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 1
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: oauth2-proxy
|
||||
namespace: dsh-demo
|
||||
labels:
|
||||
app: oauth2-proxy
|
||||
app.kubernetes.io/part-of: dsh-demo
|
||||
spec:
|
||||
selector:
|
||||
app: oauth2-proxy
|
||||
ports:
|
||||
- name: http
|
||||
port: 4180
|
||||
targetPort: http
|
||||
@@ -0,0 +1,39 @@
|
||||
# EXAMPLE ONLY — do not apply as-is.
|
||||
#
|
||||
# oauth2-proxy reads its GitHub OAuth credentials and cookie-signing secret from
|
||||
# this Secret. Populate real values and apply:
|
||||
#
|
||||
# kubectl -n dsh-demo create secret generic oauth2-proxy \
|
||||
# --from-literal=client-id='<GitHub OAuth App client ID>' \
|
||||
# --from-literal=client-secret='<GitHub OAuth App client secret>' \
|
||||
# --from-literal=cookie-secret="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
|
||||
#
|
||||
# The cookie secret must decode (base64) to exactly 16, 24, or 32 bytes.
|
||||
# oauth2-proxy base64-decodes it, so it must be URL-safe, unpadded base64.
|
||||
# Generate it with either of:
|
||||
#
|
||||
# python3 -c 'import secrets; print(secrets.token_urlsafe(32))'
|
||||
# openssl rand -base64 32 | tr -- '+/' '-_' | tr -d '='
|
||||
#
|
||||
# Do NOT use plain `openssl rand -base64 32`: that yields 44 characters of
|
||||
# standard base64 (with "=" and possibly "+"/"/"), which oauth2-proxy cannot
|
||||
# decode and therefore treats as 44 raw bytes, failing with "cookie_secret must
|
||||
# be 16, 24, or 32 bytes ... but is 44 bytes".
|
||||
#
|
||||
# GitHub OAuth App (https://github.com/settings/developers):
|
||||
# Homepage URL https://dsh-demo.example.com
|
||||
# Authorization callback https://dsh-demo.example.com/oauth2/callback
|
||||
# (replace dsh-demo.example.com with the actual public FQDN)
|
||||
#
|
||||
# NOTE: pass the base64 value as-is via --from-literal (kubectl stores the
|
||||
# literal; it does not base64-encode it a second time).
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: oauth2-proxy
|
||||
namespace: dsh-demo
|
||||
type: Opaque
|
||||
stringData:
|
||||
client-id: "<github-oauth-app-client-id>"
|
||||
client-secret: "<github-oauth-app-client-secret>"
|
||||
cookie-secret: "<urlsafe-unpadded-base64-of-32-random-bytes>"
|
||||
@@ -0,0 +1,17 @@
|
||||
# EXAMPLE ONLY — do not apply as-is.
|
||||
#
|
||||
# Copy to tls-secret.yaml and populate with real base64-encoded PEM material,
|
||||
# then apply. The secret must live in the dsh-demo namespace and be named
|
||||
# "dsh-demo-tls" (referenced by 70-gateway.yaml).
|
||||
#
|
||||
# kubectl -n dsh-demo create secret tls dsh-demo-tls \
|
||||
# --cert=tls.crt --key=tls.key --dry-run=client -o yaml > tls-secret.yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: dsh-demo-tls
|
||||
namespace: dsh-demo
|
||||
type: kubernetes.io/tls
|
||||
data:
|
||||
tls.crt: <base64-encoded-certificate>
|
||||
tls.key: <base64-encoded-private-key>
|
||||
@@ -0,0 +1,52 @@
|
||||
# OPTIONAL browser entry point: expose the gateway through Traefik.
|
||||
#
|
||||
# This is the "no SSO" exposure variant and is MUTUALLY EXCLUSIVE with
|
||||
# ../gateway-api-sso/ — apply exactly one of them (see ../README.md).
|
||||
#
|
||||
# Requires the base manifests (Service dsh-gateway:8080 from
|
||||
# manifests/50-gateway.yaml) and a Traefik install with an IngressClass named
|
||||
# "traefik" (k3s ships one). Replace the placeholder host below with a name
|
||||
# that resolves to your ingress address, and either supply a real TLS Secret or
|
||||
# delete the `tls:` block for plain HTTP.
|
||||
#
|
||||
# Two things must match this host:
|
||||
# 1. SandboxTemplate (manifests/10-sandboxtemplate.yaml): set
|
||||
# DSH_TRUSTED_HOSTS to this host so DSH accepts the forwarded Host/Origin
|
||||
# on /api.
|
||||
# 2. Gateway Deployment (manifests/50-gateway.yaml): set COOKIE_SECURE to
|
||||
# "true" once this Ingress serves HTTPS, so the session cookie is Secure.
|
||||
# The base manifest sets "false" for the plain-HTTP port-forward demo.
|
||||
#
|
||||
# WebSocket support is REQUIRED: the browser talks to /u/<user>/... and DSH
|
||||
# uses WebSockets for live session traffic. Traefik upgrades WS/SSE
|
||||
# transparently for ordinary HTTP routers, but do NOT attach buffering
|
||||
# middlewares (e.g. compress, buffering) to this router — they break streaming
|
||||
# and upgrades. `path: /` Prefix covers the landing page and every per-user
|
||||
# proxy path, so no per-sandbox Ingress is needed.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: dsh-gateway
|
||||
namespace: dsh-demo
|
||||
# Typical Traefik + cert-manager wiring (uncomment and adapt):
|
||||
# annotations:
|
||||
# traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
# cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
annotations: {}
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- dsh.example.com # placeholder
|
||||
secretName: dsh-example-tls # placeholder; create/PKI-provision this Secret
|
||||
rules:
|
||||
- host: dsh.example.com # placeholder
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: dsh-gateway
|
||||
port:
|
||||
number: 8080
|
||||
Reference in New Issue
Block a user