Skip to main content
Version: 0.2.4

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.

You bring the certificate

The platform ships the routing rules and the ingress proxy. The DNS names, the certificate and the LoadBalancer address are yours. Step 3 shows where the certificate goes.

Before you start​

  • Install complete, all pods running
  • Two DNS names you control, one for the dashboard and one for the APIs, both 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 both names, as PEM files: the full chain and its private key. One certificate that lists both names, a wildcard, or one certificate per name. Not needed if TLS terminates in front of the ingress

1. The two addresses​

The dashboard and the agent-facing APIs are published on separate addresses, for example:

AddressServesSet as
dashboard.gen0sec.example.internalThe dashboard. Its sign-in URL and passkeys follow this nameservices.ui.ingress.hosts[0].host
api.gen0sec.example.internalEvery agent-facing API, routed by path prefixentrypointHost

Both names point at the same ingress address. Use the values you set in Install step 7. They are already in your shell if you followed the install in it:

export DASHBOARD_HOST=dashboard.gen0sec.example.internal
export ENTRYPOINT_HOST=api.gen0sec.example.internal

Pass both in every platform install and upgrade command. Leave out the dashboard's and the dashboard moves onto the API address, and sign-in and passkeys break. The double quotes around the dashboard's --set expand the variable and stop zsh reading [0] as a glob.

The API address 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 dashboard address people will actually type. The dashboard derives its sign-in URL from it, and sign-in fails if the two do not match.

WhoUses
People, in the browser, and first sign-inhttps://$DASHBOARD_HOST
Agents, and verify-deployment.shhttps://$ENTRYPOINT_HOST

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 "services.ui.ingress.hosts[0].host=$DASHBOARD_HOST" \
--set global.imageTag=$VERSION \
--timeout 20m

The overlay's name and header speak of one address. The dashboard host you pass moves the dashboard to its own; every API stays on entrypointHost.

Re-pass every values file you installed with

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 "services.ui.ingress.hosts[0].host=$DASHBOARD_HOST" \
--set global.imageTag=$VERSION \
--timeout 20m

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.

The certificate has to cover both names. Pick the case that matches what you have.

One certificate for both names, listing both or with a wildcard such as *.gen0sec.example.internal:

  1. Create the secret in gen0sec-system, the proxy's namespace, not gen0sec. --cert takes the full chain: your certificate followed by any intermediates.

    kubectl -n gen0sec-system create secret tls cerebellum-tls \
    --cert=fullchain.pem --key=privkey.pem
  2. Write a values file that mounts it:

    cat > ingress-tls-values.yaml <<EOF
    synapse:
    volumes:
    - name: ingress-tls
    secret:
    secretName: cerebellum-tls
    items:
    - { key: tls.crt, path: $ENTRYPOINT_HOST.crt }
    - { key: tls.key, path: $ENTRYPOINT_HOST.key }
    volumeMounts:
    - name: ingress-tls
    mountPath: /etc/synapse/certs
    readOnly: true
    EOF

    With one certificate mounted, the proxy serves it for both names.

One certificate per name:

  1. Create a secret for each in gen0sec-system, from each name's full chain and key:

    kubectl -n gen0sec-system create secret tls dashboard-tls \
    --cert=dashboard-fullchain.pem --key=dashboard-privkey.pem
    kubectl -n gen0sec-system create secret tls api-tls \
    --cert=api-fullchain.pem --key=api-privkey.pem
  2. Write a values file that mounts both into the same folder, each named after its address:

    cat > ingress-tls-values.yaml <<EOF
    synapse:
    volumes:
    - name: ingress-tls
    projected:
    sources:
    - secret:
    name: dashboard-tls
    items:
    - { key: tls.crt, path: $DASHBOARD_HOST.crt }
    - { key: tls.key, path: $DASHBOARD_HOST.key }
    - secret:
    name: api-tls
    items:
    - { key: tls.crt, path: $ENTRYPOINT_HOST.crt }
    - { key: tls.key, path: $ENTRYPOINT_HOST.key }
    volumeMounts:
    - name: ingress-tls
    mountPath: /etc/synapse/certs
    readOnly: true
    EOF

    The proxy picks the certificate by the name the client asks for.

Then, for either case:

  1. 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.yaml

    On the Quickstart, also pass -f $KIT/examples/single-node/infra-values.yaml, before this file. Keep ingress-tls-values.yaml with your other overrides: every later infra upgrade must pass it too, or the mount is removed and port 443 closes.

  2. Confirm the proxy loaded it:

    kubectl -n gen0sec-system rollout status deploy/g0s-infra-synapse
    kubectl -n gen0sec-system logs deploy/g0s-infra-synapse | grep -i "certificate selection"

    Wait for the rollout first. While the old pod is still running, logs can 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 default was not found is expected. No certificate is named default, so the proxy falls back to the first one it loaded for a name it has no file for.

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 a certificate, replace the secret's contents. The proxy picks up the new one within about two minutes, without a restart. With one certificate per name, do this for dashboard-tls and api-tls in turn:

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 each Certificate write to its 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 api.gen0sec.example.internal 80 30s
g0s-ban-ledger-api synapse api.gen0sec.example.internal 80 30s
g0s-config-api synapse api.gen0sec.example.internal 80 30s
g0s-data-api synapse api.gen0sec.example.internal 80 30s
g0s-download-api synapse api.gen0sec.example.internal 80 30s
g0s-feeds-api synapse api.gen0sec.example.internal 80 30s
g0s-service-graph-api synapse api.gen0sec.example.internal 80 30s
g0s-signal-api synapse api.gen0sec.example.internal 80 30s
g0s-telemetry-api synapse api.gen0sec.example.internal 80 30s
g0s-ui synapse dashboard.gen0sec.example.internal 80 30s

There is one Ingress per published service, ten in all. g0s-ui shows the dashboard address and the other nine the API address. The HOSTS column is the one to check: if g0s-ui shows the API address, the dashboard host was not passed. 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 / of its address and returns a redirect to its sign-in page, so follow it with -L:

curl -fsSL -o /dev/null -w '%{http_code}\n' https://$DASHBOARD_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.

The API address answers 404 on /. That is expected: it serves only the paths in the route table. The smoke test checks the APIs.

There is no health endpoint on the ingress

/status/healthz is served per pod inside the cluster and is deliberately not published through the ingress. From outside it returns 404 on the API address, and the dashboard's 307 redirect to its sign-in page on the dashboard address, never 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 both DNS names 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 prefixServed by
/, on the dashboard addressDashboard. The only route on that address
/v1/indicators, /v1/models, /v1/geoip, /v1/ids-rulesRelay
/v1/penalty-model, /v1/traffic-model, /v1/ja4-classifier, /v1/flow-classifier, /v1/jepa-embedderRelay, per-model aliases
/v1/identity, /v1/policy-edges, /v1/fleet-bansRelay, served locally
/v1/authcheck, /v1/authAuthentication
/v1/events, /v1/agents, /v1/logAgent ingest
/v1/agents/eventsConfiguration push (longer prefix than /v1/agents, so it wins)
/v1/configConfiguration
/v1/telemetry, /v1/logsTelemetry
/v1/signal, /v1/rulesSignal
/v1/ban-ledgerBan ledger
/v1/threatThreat feeds
/v1/service-graphService graph

Every other row is on the API address. Routing there is by longest matching prefix, because one address serves every API.

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.

Give agents the API address, never the dashboard's

On the dashboard address the dashboard owns /, so every API path 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 an agent pointed at the dashboard address fails silently:

RequestWhat the agent actually does
/v1/authcheckReceives HTML, cannot distinguish a bad key from a routing mistake, and continues unverified
/v1/agents/eventsLogs SSE connect returned HTTP 404, then falls back to polling. Looks like a slow platform, not a routing bug
/v1/ban-ledgerReads the redirect as success. The contribution is lost with no error anywhere

On the API address, a path missing from this table gets a plain 404 instead.

/v1/ids-rules goes to the relay, not to the in-cluster rules service

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 its own 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​

SymptomCause
helm upgrade fails with entrypointHost is requiredExpected, and the point: entrypointHost was not passed. Set it and re-run
kubectl get ingress shows an empty HOSTS columnAn 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 originThe dashboard's URL does not match the address people type. Check services.ui.ingress.hosts[0].host, and that externalScheme matches what people type: https unless they browse over plain HTTP
g0s-ui shows the API address, or the dashboard address returns 404The dashboard host was not passed in the last platform upgrade. Re-run it with --set "services.ui.ingress.hosts[0].host=$DASHBOARD_HOST"
curl times outDNS does not resolve to your ingress controller, or a firewall blocks the port
curl https:// is refused, http:// worksNo certificate is mounted, so port 443 is closed. The proxy logs TLS listener disabled. See step 3
HTTPS worked, then stopped after an infra upgradeThe 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 certificateThe secret is in the wrong namespace or was not replaced. It must be in gen0sec-system, under the name your ingress-tls-values.yaml mounts
One address has the wrong certificateWith one certificate per name, a file is not named after its address. Check the path: entries in ingress-tls-values.yaml
The dashboard loads but sign-in redirects to the wrong addressThe sign-in URL is derived from services.ui.ingress.hosts[0].host, or from entrypointHost when that was left out. It must be the address people type
A request reaches the wrong serviceLongest-prefix routing. Check the /v1/agents overlap above