Publish the endpoints
The ingress controller is installed and running. However it's not configured to serve anything by default. That is deliberate, because creating those routes makes the APIs reachable from wherever the controller is reachable, so it is your decision, not ours.
The data plane's LoadBalancer Service already has an external address at this point. Until you complete this page it serves nothing, because no rule names a host.
The platform ships the routing rules and the ingress proxy. The DNS name, the certificate and the LoadBalancer address are yours. Step 3 shows where the certificate goes.
Before you start
- Install complete, all pods running
- A DNS name you control, resolving to your ingress controller's address
- A LoadBalancer address for the ingress data plane, or a Service type your cluster can expose
- A TLS certificate for that name, as PEM files: the full chain and its private key. Not needed if TLS terminates in front of the ingress
1. Choose the address
One address serves the dashboard and every agent-facing API. Requests are routed by path prefix.
Use the address you set as entrypointHost in Install step 7.
It is already in $ENTRYPOINT_HOST if you followed the install in this shell:
export ENTRYPOINT_HOST=cerebellum.example.internal
The host is required, and the chart enforces it. A rule with no host is silently ignored by the
ingress controller: the Ingress object is created, no event is logged, and nothing serves traffic.
Rather than publish one, the chart refuses to render and names the value to set, so this fails at
helm upgrade instead of in production.
Use the address people will actually type. The dashboard derives its sign-in URL from this host, and sign-in fails if the two do not match.
Or: the dashboard on its own address
The dashboard can have its own name, with every agent-facing API on another, for example
dashboard.gen0sec.example.internal and api.gen0sec.example.internal. Point both names at the same
ingress address.
entrypointHostis the API address. Every API rule is published under it.services.ui.ingress.hosts[0].hostis the dashboard address. The dashboard's sign-in URL and its passkeys follow it.
export ENTRYPOINT_HOST=api.gen0sec.example.internal
export DASHBOARD_HOST=dashboard.gen0sec.example.internal
Add this next to entrypointHost in every platform install and upgrade command. The double quotes
expand the variable and stop zsh reading [0] as a glob:
--set "services.ui.ingress.hosts[0].host=$DASHBOARD_HOST"
Where these pages say $ENTRYPOINT_HOST, use:
| For | Use |
|---|---|
The dashboard: the browser, the curl check on /, first sign-in | $DASHBOARD_HOST |
Agents, and verify-deployment.sh | $ENTRYPOINT_HOST |
On the API address, / and any path not in the route table return a plain 404,
because the dashboard's catch-all route lives on its own address.
The certificate has to cover both names. See step 3.
2. Publish
The kit ships the overlay. Layer it on top of the values file you have been using.
helm upgrade g0s $B/charts/gen0sec-platform-*.tgz \
-n gen0sec \
-f $B/values/gen0sec-platform-values-onprem.yaml \
-f $KIT/examples/single-entrypoint/ingress-values.yaml \
--set entrypointHost=$ENTRYPOINT_HOST \
--set global.imageTag=$VERSION \
--timeout 20m
helm upgrade applies only the files you pass it. Anything layered at install time and omitted
here reverts to the chart default. On the Quickstart you also passed
-f $KIT/examples/single-node/platform-values.yaml, so include it in this upgrade too (before the
ingress overlay), or every service scales back to its production replica count:
helm upgrade g0s $B/charts/gen0sec-platform-*.tgz \
-n gen0sec \
-f $B/values/gen0sec-platform-values-onprem.yaml \
-f $KIT/examples/single-node/platform-values.yaml \
-f $KIT/examples/single-entrypoint/ingress-values.yaml \
--set entrypointHost=$ENTRYPOINT_HOST \
--set global.imageTag=$VERSION \
--timeout 20m
zsh treats [0] as a glob and fails before helm runs. Single-quote the whole argument:
--set 'services.ui.ingress.hosts[0].host=...'.
Routes take about a minute to appear. The proxy re-reads its routing table from a ConfigMap, which
reaches the pod about a minute after the upgrade. Until then, every path returns 404.
3. Add the certificate
Skip this step if a load balancer, CDN or tunnel terminates TLS in front of the ingress; see If TLS terminates upstream.
The ingress proxy reads its certificates from a folder, /etc/synapse/certs. It does not take
them from the tls: section of an Ingress, and out of the box nothing is mounted in that folder, so
the proxy never opens port 443. You mount your certificate there through the infra chart.
-
Create the secret in
gen0sec-system, the proxy's namespace, notgen0sec.--certtakes the full chain: your certificate followed by any intermediates.kubectl -n gen0sec-system create secret tls cerebellum-tls \--cert=fullchain.pem --key=privkey.pem -
Write a values file that mounts it, with the files named after your address:
cat > ingress-tls-values.yaml <<EOFsynapse:volumes:- name: ingress-tlssecret:secretName: cerebellum-tlsitems:- { key: tls.crt, path: $ENTRYPOINT_HOST.crt }- { key: tls.key, path: $ENTRYPOINT_HOST.key }volumeMounts:- name: ingress-tlsmountPath: /etc/synapse/certsreadOnly: trueEOFDashboard on its own address? One certificate that covers both names, by listing both or with a wildcard, needs nothing more: mount it as above. With a certificate for each name, create a secret for each as in step 1, here
dashboard-tlsandapi-tls, and mount both into the same folder. Use thisvolumesin the file above and keep itsvolumeMounts:synapse:volumes:- name: ingress-tlsprojected:sources:- secret:name: dashboard-tlsitems:- { key: tls.crt, path: dashboard.gen0sec.example.internal.crt }- { key: tls.key, path: dashboard.gen0sec.example.internal.key }- secret:name: api-tlsitems:- { key: tls.crt, path: api.gen0sec.example.internal.crt }- { key: tls.key, path: api.gen0sec.example.internal.key }The proxy picks the certificate by the name the client asks for.
-
Upgrade infra with it, after every file you installed infra with:
helm upgrade g0s-infra $B/charts/gen0sec-infra-*.tgz \-n gen0sec-system \-f $B/values/gen0sec-infra-values-onprem.yaml \-f ingress-tls-values.yamlOn the Quickstart, also pass
-f $KIT/examples/single-node/infra-values.yaml, before this file. Keepingress-tls-values.yamlwith your other overrides: every later infra upgrade must pass it too, or the mount is removed and port 443 closes. -
Confirm the proxy loaded it:
kubectl -n gen0sec-system rollout status deploy/g0s-infra-synapsekubectl -n gen0sec-system logs deploy/g0s-infra-synapse | grep -i "certificate selection"Wait for the rollout first. While the old pod is still running,
logscan read it instead, and it has no certificate line.The output is similar to this:
{"level":"INFO","message":"Successfully created TlsSettings with SNI-based certificate selection",...}A warning that the default certificate
defaultwas not found is expected: with one certificate mounted, the proxy uses it for every request.
You do not need a tls: section on any Ingress, and adding one does not install a certificate. Port 80
keeps serving plain HTTP alongside 443. It does not redirect to HTTPS.
To renew the certificate, replace the secret's contents. The proxy picks up the new one within about two minutes, without a restart:
kubectl -n gen0sec-system create secret tls cerebellum-tls \
--cert=fullchain.pem --key=privkey.pem --dry-run=client -o yaml \
| kubectl apply -f -
If cert-manager issues it, have the Certificate write to that secret name in gen0sec-system. Its
renewals are picked up the same way.
If TLS terminates upstream
If a load balancer, CDN or tunnel terminates TLS in front of the ingress, point it at port 80 of the
ingress Service and skip the certificate. There is nothing else to set: the overlay already sets
externalScheme: https, which tells the dashboard its address is https:// although the request
reaches it over HTTP.
Plain HTTP, for an evaluation
With no certificate anywhere, set the scheme to match, or the dashboard builds its sign-in URL with
https while the browser is on http, and rejects every sign-in with Invalid origin:
--set externalScheme=http
Re-run the publish command in step 2 with it, and use http:// in the checks below. Pass it on every
later platform upgrade too, or the scheme goes back to https.
One service with a different scheme
services.<service>.ingress.externalScheme overrides externalScheme for one service. Only a service
that derives its own external URL reads it, and the dashboard is the only one that does.
4. Verify the routes exist
kubectl -n gen0sec get ingress
The output is similar to this:
NAME CLASS HOSTS ADDRESS PORTS AGE
g0s-auth-api synapse cerebellum.example.internal 80 30s
g0s-ban-ledger-api synapse cerebellum.example.internal 80 30s
g0s-config-api synapse cerebellum.example.internal 80 30s
g0s-data-api synapse cerebellum.example.internal 80 30s
g0s-download-api synapse cerebellum.example.internal 80 30s
g0s-feeds-api synapse cerebellum.example.internal 80 30s
g0s-service-graph-api synapse cerebellum.example.internal 80 30s
g0s-signal-api synapse cerebellum.example.internal 80 30s
g0s-telemetry-api synapse cerebellum.example.internal 80 30s
g0s-ui synapse cerebellum.example.internal 80 30s
There is one Ingress per published service, ten in all, each under your address. With the dashboard on
its own address, g0s-ui shows that one and the other nine the API address. The HOSTS column is the
one to check. An empty ADDRESS is normal with this controller, and PORTS shows 80 even
with a certificate mounted, because the certificate is not on the Ingress.
An empty HOSTS column means the host was not applied, and nothing will serve traffic. The upgrade
fails before it can leave an Ingress in that state, so if you see one it was created by an earlier
install.
Then check from outside the cluster. The dashboard is published at / and returns a redirect to
its sign-in page, so follow it with -L:
curl -fsSL -o /dev/null -w '%{http_code}\n' https://$ENTRYPOINT_HOST/
The output is similar to this:
200
A 404 within a minute of the upgrade means the routes have not loaded yet. Wait a minute and retry
before treating it as a failure.
/status/healthz is served per pod inside the cluster and is deliberately not published through
the ingress. A request to it from outside falls through to the dashboard catch-all and gets the
dashboard's 307 redirect to its sign-in page, not a health response, so do not use it as an external
check. The dashboard root above confirms reachability; the
smoke test confirms the platform serves agents.
Find the address to point DNS at
The ingress data plane is a LoadBalancer Service.
kubectl -n gen0sec-system get svc g0s-infra-synapse -o wide
Point your DNS name at the external address. If EXTERNAL-IP stays <pending>, your cluster has no
LoadBalancer implementation: change the Service type to something it can expose. It cannot stay
cluster-internal.
The route table
You do not configure these. The overlay sets them, read off each service's own router. They are here because they are the answer to "a request landed on the wrong backend", and because a missing route fails quietly rather than loudly.
| Path prefix | Served by |
|---|---|
/ | Dashboard (catch-all, shortest prefix, so it is last resort) |
/v1/indicators, /v1/models, /v1/geoip, /v1/ids-rules | Relay |
/v1/penalty-model, /v1/traffic-model, /v1/ja4-classifier, /v1/flow-classifier, /v1/jepa-embedder | Relay, per-model aliases |
/v1/identity, /v1/policy-edges, /v1/fleet-bans | Relay, served locally |
/v1/authcheck, /v1/auth | Authentication |
/v1/events, /v1/agents, /v1/log | Agent ingest |
/v1/agents/events | Configuration push (longer prefix than /v1/agents, so it wins) |
/v1/config | Configuration |
/v1/telemetry, /v1/logs | Telemetry |
/v1/signal, /v1/rules | Signal |
/v1/ban-ledger | Ban ledger |
/v1/threat | Threat feeds |
/v1/service-graph | Service graph |
Routing is by longest matching prefix, because one address serves every backend.
The configuration push stream on /v1/config is long-lived, so the chart sets read, write and idle
timeouts of 3600 seconds on that route. Do not lower them: agents fall back to polling with no push,
which looks like a slow platform rather than a timeout.
The dashboard owns /, so any path absent from this table falls through to it and gets a redirect or
HTML. HTTP has no way for the caller to tell that apart from a real answer, so the failures are
silent:
| Missing route | What the agent actually does |
|---|---|
/v1/authcheck | Receives HTML, cannot distinguish a bad key from a routing mistake, and continues unverified |
/v1/agents/events | Logs SSE connect returned HTTP 404, then falls back to polling. Looks like a slow platform, not a routing bug |
/v1/ban-ledger | Reads the redirect as success. The contribution is lost with no error anywhere |
This is why the overlay exists rather than a list of prefixes for you to type.
IDS rules are relayed from gen0sec. The in-cluster rules service builds rules from a local ingest pipeline that this edition does not run. Route this prefix there and you reach a service with no rules, and the relay is never used.
The dashboard
The dashboard is enabled by default and is published at the root of your address. Sign-in is email and password, held in your own Postgres, so nothing outside the cluster is involved.
Its secret, g0s-db-ui, was created in Install step 6 with all the others.
Re-running that script keeps the existing signing secret, so sessions and invite links stay valid.
To leave the dashboard out:
--set services.ui.enabled=false
If it fails
| Symptom | Cause |
|---|---|
helm upgrade fails with entrypointHost is required | Expected, and the point: entrypointHost was not passed. Set it and re-run |
kubectl get ingress shows an empty HOSTS column | An Ingress left by an earlier install, created when a host-less rule could still render. Re-run the upgrade with entrypointHost set |
Sign-in is rejected with Invalid origin | The dashboard's URL does not match the address people type. Check entrypointHost, and that externalScheme matches what people type: https unless they browse over plain HTTP |
curl times out | DNS does not resolve to your ingress controller, or a firewall blocks the port |
curl https:// is refused, http:// works | No certificate is mounted, so port 443 is closed. The proxy logs TLS listener disabled. See step 3 |
| HTTPS worked, then stopped after an infra upgrade | The upgrade did not pass ingress-tls-values.yaml, so the mount was removed. Re-run it with the file |
| The browser shows an old or wrong certificate | The secret is in the wrong namespace or was not replaced. It must be cerebellum-tls in gen0sec-system |
| The dashboard loads but sign-in redirects to the wrong address | The sign-in URL is derived from the dashboard's address: services.ui.ingress.hosts[0].host if set, otherwise entrypointHost. It must be the address people type |
| A request reaches the wrong service | Longest-prefix routing. Check the /v1/agents overlap above |