Production Kubernetes Deployment¶
This runbook walks through deploying ShoreGuard alongside NVIDIA OpenShell gateways on a production Kubernetes cluster. The two are installed as separate Helm releases — ShoreGuard does not bundle or manage NVIDIA's chart.
What this guide covers:
- Installing NVIDIA's upstream OpenShell Helm chart (pointer to their docs)
- Installing
charts/shoreguardwith the production preset - Registering gateways with mTLS
- Verifying the deployment end-to-end
What this guide does not cover: umbrella charts, GitOps manifests, Terraform modules, or operator-based deployments.
Prerequisites¶
| Requirement | Minimum | Notes |
|---|---|---|
| Kubernetes | >= 1.25 | Tested against kind v1.32 |
| Helm | >= 3.12 | helm version |
| kubectl | matching cluster version | |
| CNI | calico or cilium | Must enforce NetworkPolicy — kindnet and basic flannel do not |
| cert-manager | >= 1.14 | With a ClusterIssuer named letsencrypt-prod |
| Ingress controller | ingress-nginx | The production preset sets ingress.className: nginx |
| StorageClass | any RWO-capable | Cluster default works; override via persistence.storageClassName |
Pod Security Standards
ShoreGuard runs under the restricted PSS profile (non-root, read-only
root filesystem, all capabilities dropped). NVIDIA's OpenShell chart
may require a privileged namespace — check their docs and label the
namespace accordingly:
kubectl label ns openshell pod-security.kubernetes.io/enforce=privileged
Step 1 — Install NVIDIA OpenShell¶
Install NVIDIA's upstream OpenShell Helm chart into a dedicated namespace. ShoreGuard has been tested against OpenShell v0.0.26 — pin to that version unless you have verified interop with a newer release.
Where to find the chart
Refer to NVIDIA's OpenShell documentation for installation instructions. ShoreGuard does not ship, vendor, or manage NVIDIA's chart — install it exactly as NVIDIA documents.
kubectl create ns openshell
kubectl label ns openshell pod-security.kubernetes.io/enforce=privileged
# Install NVIDIA's chart per their docs, e.g.:
# helm install gw-prod nvidia/openshell --namespace openshell \
# --set <nvidia-specific-values>
After installation, verify the gateway pod is running and the mTLS Secrets have been created:
Note the Secret name(s) containing the client TLS material — you will need them in Step 4.
Step 2 — Create the ShoreGuard namespace and secrets¶
Create a Secret with the admin bootstrap password and session HMAC key:
kubectl -n shoreguard create secret generic sg-secrets \
--from-literal=admin-password="$(openssl rand -hex 16)" \
--from-literal=secret-key="$(openssl rand -hex 32)"
Secret management
For production, consider External Secrets Operator, Sealed Secrets, or
HashiCorp Vault instead of plain kubectl create secret. The chart
accepts any Secret that contains the keys admin-password and
secret-key.
Save the admin password — you will need it to log in:
kubectl -n shoreguard get secret sg-secrets \
-o jsonpath='{.data.admin-password}' | base64 -d; echo
Step 3 — Install ShoreGuard¶
Use values.production.yaml as the base and override the values specific
to your environment:
helm install sg ./charts/shoreguard \
--namespace shoreguard \
-f charts/shoreguard/values.production.yaml \
--set existingSecret=sg-secrets \
--set ingress.host=shoreguard.example.com \
--set 'ingress.tls[0].hosts[0]=shoreguard.example.com' \
--set 'ingress.tls[0].secretName=shoreguard-tls' \
--set networkPolicy.egress.inClusterGateways.enabled=true \
--set 'networkPolicy.egress.inClusterGateways.namespaceSelector.matchLabels.kubernetes\.io/metadata\.name=openshell'
Replace shoreguard.example.com with your actual domain. The production
preset enables:
- Persistence — 2 Gi PVC at
/home/shoreguard/.config/shoreguard - Ingress — nginx class with cert-manager TLS and force-SSL-redirect
- NetworkPolicy — DNS + LLM providers (443/tcp non-RFC1918) egress
- JSON logging — structured output for log aggregation
- Forwarded header trust —
forwardedAllowIps: "*"so the app sees the real client IP through the Ingress controller
The inClusterGateways egress rule opens TCP 30051 to pods in the
openshell namespace, allowing ShoreGuard to reach the gateways through
the NetworkPolicy.
Optional: podSelector for gateway egress
If you want to restrict egress to specific gateway pods (not all pods
in the namespace), add a podSelector:
--set 'networkPolicy.egress.inClusterGateways.podSelector.matchLabels.app\.kubernetes\.io/name=openshell'
Check kubectl -n openshell get pods --show-labels for the correct
label key/value from NVIDIA's chart.
Wait for the rollout and run the built-in helm test:
kubectl -n shoreguard wait --for=condition=ready pod \
-l app.kubernetes.io/name=shoreguard --timeout=120s
helm test sg --namespace shoreguard
Optional overrides¶
| Override | When to use |
|---|---|
--set database.url=postgresql://... |
External Postgres (required for multi-replica) |
--set replicaCount=2 |
Horizontal scaling — needs database.url + existingSecret or explicit secretKey |
--set podDisruptionBudget.enabled=true |
Enable PDB when running multiple replicas |
--set persistence.storageClassName=... |
Non-default StorageClass |
--set networkPolicy.egress.postgres.enabled=true |
In-cluster Postgres behind NetworkPolicy |
Multi-replica footguns
The chart refuses to render if replicaCount > 1 without
database.url (SQLite + RWO PVC deadlock) or without a stable
secretKey / existingSecret (session HMAC mismatch across replicas).
Step 4 — Register gateways¶
Once ShoreGuard is running, register each OpenShell gateway with its mTLS
credentials. You can do this via the ShoreGuard UI or via curl.
Extract mTLS material¶
Find the client TLS Secret that NVIDIA's chart created:
Secret name
The exact Secret name depends on NVIDIA's chart version and your Helm
release name. Look for a Secret containing ca.crt, tls.crt, and
tls.key (or similar keys). Adjust the jsonpath expressions below
to match.
Extract and base64-encode the certificates:
export GW_RELEASE=gw-prod # your NVIDIA Helm release name
export GW_NS=openshell
CA_CERT=$(kubectl -n "$GW_NS" get secret "${GW_RELEASE}-client-tls" \
-o jsonpath='{.data.ca\.crt}')
CLIENT_CERT=$(kubectl -n "$GW_NS" get secret "${GW_RELEASE}-client-tls" \
-o jsonpath='{.data.tls\.crt}')
CLIENT_KEY=$(kubectl -n "$GW_NS" get secret "${GW_RELEASE}-client-tls" \
-o jsonpath='{.data.tls\.key}')
Register via curl¶
# Determine the ShoreGuard URL
SG_URL="https://shoreguard.example.com"
# Or via port-forward:
# kubectl -n shoreguard port-forward svc/sg-shoreguard 8888:8888 &
# SG_URL="http://127.0.0.1:8888"
# Retrieve admin password
ADMIN_PW=$(kubectl -n shoreguard get secret sg-secrets \
-o jsonpath='{.data.admin-password}' | base64 -d)
# Login
curl -c cookies.txt -X POST "${SG_URL}/api/auth/login" \
-H 'Content-Type: application/json' \
-d "{\"email\":\"admin@localhost\",\"password\":\"${ADMIN_PW}\"}"
# Register gateway
curl -b cookies.txt -X POST "${SG_URL}/api/gateway/register" \
-H 'Content-Type: application/json' \
-d "{
\"name\": \"${GW_RELEASE}\",
\"endpoint\": \"${GW_RELEASE}-openshell.${GW_NS}.svc.cluster.local:30051\",
\"scheme\": \"https\",
\"auth_mode\": \"mtls\",
\"labels\": {\"env\": \"production\"},
\"ca_cert\": \"${CA_CERT}\",
\"client_cert\": \"${CLIENT_CERT}\",
\"client_key\": \"${CLIENT_KEY}\"
}"
Registering via the UI
Navigate to Gateways in the ShoreGuard UI, click Register Gateway, and paste the endpoint, certificates, and labels into the form. The UI accepts the same fields as the API.
Repeat for each gateway, adjusting the release name, endpoint, and labels.
Step 5 — Verify¶
Run through this checklist after registration:
-
[ ] Helm test passes:
helm test sg --namespace shoreguard -
[ ] Gateway connected:
Each gateway should show"status": "connected". -
[ ] Label filter works:
-
[ ] UI login works at
https://shoreguard.example.comwithadmin@localhostand the password from Step 2. -
[ ] TLS certificate issued:
The certificate should showReady=True. -
[ ] Persistence survives restart:
Gateways and settings should still be present.
Day-2 operations¶
Scaling to multiple replicas¶
- Set up an external PostgreSQL database.
- Upgrade with
--set database.url=postgresql://... --set replicaCount=2 --set podDisruptionBudget.enabled=true. - The chart validates that
secretKeyorexistingSecretis set whenreplicaCount > 1.
Secret rotation¶
Updating sg-secrets does not trigger a pod restart. After rotating
keys: