Install
Before you start
- Prepare the artifacts complete
-
$KIT,$B,$REGISTRY,$REGISTRY_USER,$REGISTRY_TOKENare set in your shell -
$G0S_DOWNLOAD_API_KEYis set. Step 2 writes it into a secret, and an unset variable produces an empty one that fails silently - You are logged in to the registry
1. Check the cluster
$KIT/scripts/preflight.sh
This checks the prerequisites the charts assume but do not create: a reachable cluster, a StorageClass, and the namespaces and secrets created in step 2. On a fresh cluster it reports the namespaces and secrets as missing, which is expected at this point.
Pass a StorageClass name to pin it instead of accepting the default:
$KIT/scripts/preflight.sh gen0sec-system gen0sec my-block-storage
2. Create the namespaces and the two bootstrap secrets
for ns in gen0sec-system gen0sec; do
kubectl create namespace $ns --dry-run=client -o yaml | kubectl apply -f -
done
Written to converge, so re-running step 2 after a failure further down is safe rather than a wall of
AlreadyExists.
The image pull secret
Both namespaces need it. The application runs in gen0sec; the operators, object store and cache run
in gen0sec-system.
for ns in gen0sec-system gen0sec; do
kubectl -n $ns create secret docker-registry gen0sec-registry \
--docker-server=$REGISTRY \
--docker-username=$REGISTRY_USER \
--docker-password=$REGISTRY_TOKEN \
--dry-run=client -o yaml | kubectl apply -f -
done
The object store credentials
Unlike the rest of step 2 this does not converge. It mints new random keys every time it runs, and the object store's existing data was written with the old ones — recreating the secret on a second pass strands that data behind credentials nothing holds any more. The guard below stops before that happens.
kubectl -n gen0sec-system get secret gen0sec-object-store >/dev/null 2>&1 \
&& echo "already exists — skip to step 3" \
|| echo "not present — run the block below"
umask 077
S=$(mktemp -d)
openssl rand -hex 16 | tr -d '\n' > "$S/RUSTFS_ACCESS_KEY"
openssl rand -hex 32 | tr -d '\n' > "$S/RUSTFS_SECRET_KEY"
kubectl -n gen0sec-system create secret generic gen0sec-object-store --from-file="$S"
Save both files somewhere safe before you delete them. Nothing regenerates these keys, and the data in the object store is written with them.
mkdir -p ~/gen0sec-object-store-keys
cp "$S"/RUSTFS_* ~/gen0sec-object-store-keys/ # or load them into your secret manager
rm -rf "$S"
Step 5 reads them back from that directory.
Your Gen0Sec API key
The relay presents this key to Gen0Sec when it fetches threat intelligence, GeoIP, models and IDS rules. The chart expects this exact secret name, so the relay pod will not start without it. See Credentials from gen0sec.
# Refuses to run rather than creating an empty secret. An empty key is accepted
# by Kubernetes and by the chart, and only surfaces later as every relay fetch
# returning 401 across the whole fleet — with nothing in the install log.
: "${G0S_DOWNLOAD_API_KEY:?not set — see Prepare the artifacts, step 0}"
kubectl -n gen0sec create secret generic gen0sec-download-proxy \
--from-literal=DOWNLOAD_PROXY_API_KEY=$G0S_DOWNLOAD_API_KEY
3. Install infra
helm install g0s-infra $B/charts/gen0sec-infra-*.tgz \
-n gen0sec-system \
-f $B/values/gen0sec-infra-values-onprem.yaml
This installs three operators (Postgres, Kafka, ingress), the object store and the shared cache.
Wait for all five
The next step creates resources these operators have to reconcile. Do not continue until every one is available.
kubectl -n gen0sec-system rollout status deploy/g0s-infra-postgres-operator
kubectl -n gen0sec-system rollout status deploy/strimzi-cluster-operator
kubectl -n gen0sec-system rollout status deploy/g0s-infra-synapse-operator
kubectl -n gen0sec-system rollout status deploy/dragonfly
kubectl -n gen0sec-system rollout status \
"$(kubectl -n gen0sec-system get sts,deploy -o name | grep -m1 rustfs)"
The output for each is similar to this:
deployment "g0s-infra-postgres-operator" successfully rolled out
The object store is a StatefulSet in the production configuration and a Deployment in the single-node configuration. Resolving the kind means the same command works in both.
4. Install data
helm install g0s-data $B/charts/gen0sec-data-*.tgz \
-n gen0sec-system \
-f $B/values/gen0sec-data-values-onprem.yaml
This creates the Postgres cluster, the Kafka cluster, its topics, and the database roles.
Wait for both clusters
Both take several minutes. Nothing after this works until Postgres reports Running.
kubectl -n gen0sec-system wait --for=condition=Ready kafka/core --timeout=15m
kubectl -n gen0sec-system wait \
--for=jsonpath='{.status.PostgresClusterStatus}'=Running postgresql/core --timeout=15m
5. Create the object store bucket
The object store starts empty and nothing creates the bucket for you. Create it now, before the platform install. Services that read or write the object store fail until it exists.
This step is required on every install. The two datasets your cluster produces about itself,
identity and policy-edges, live here and are never sent anywhere. See
Network and connectivity.
Run it as a throwaway pod in the cluster. The pod reads the keys straight from the
gen0sec-object-store secret, so they never reach your shell and never touch your local AWS
configuration.
kubectl -n gen0sec-system run s3-init --rm -i --restart=Never \
--image=amazon/aws-cli:2.15.0 \
--overrides='{"spec":{"containers":[{"name":"s3-init","image":"amazon/aws-cli:2.15.0",
"env":[{"name":"AWS_ACCESS_KEY_ID","valueFrom":{"secretKeyRef":{"name":"gen0sec-object-store","key":"RUSTFS_ACCESS_KEY"}}},
{"name":"AWS_SECRET_ACCESS_KEY","valueFrom":{"secretKeyRef":{"name":"gen0sec-object-store","key":"RUSTFS_SECRET_KEY"}}},
{"name":"AWS_EC2_METADATA_DISABLED","value":"true"},
{"name":"AWS_REGION","value":"us-east-1"}],
"args":["--endpoint-url","http://rustfs-svc.gen0sec-system.svc.cluster.local:9000","s3","mb","s3://platform-data"]}]}}'
Expect make_bucket: platform-data. BucketAlreadyOwnedByYou is also a pass, so this step
converges: running it twice is harmless.
amazon/aws-cli is not in the offline bundle. The bundle carries only what
release-manifest.yaml pins, and its SHA256SUMS covers the whole tree — the bundler refuses a
package containing anything the checksums do not list — so it cannot be added after the fact.
Mirror it alongside the bundle, then point the pod at your copy:
skopeo copy docker://amazon/aws-cli:2.15.0 docker://$REGISTRY/amazon/aws-cli:2.15.0
Substitute $REGISTRY/amazon/aws-cli:2.15.0 for amazon/aws-cli:2.15.0 in both the --image flag
and the image field of the overrides above. They must match: the --image flag sets the pod's
image, and the override object sets the container's, and Kubernetes uses the latter.
Verify by running the same command with "s3","ls" in place of "s3","mb","s3://platform-data".
The bucket should be listed:
2026-09-01 14:02:11 platform-data
An empty result means the bucket was not created, and the services in step 7 will fail with
500 NoSuchBucket. Do not continue.
platform-data is the value of global.commonEnv.S3_BUCKET. If you overrode it, use your name
instead.
The in-cluster method above is recommended because the credentials never enter your shell. That matters most on managed Kubernetes.
On EKS, exporting them breaks kubectl. The kubeconfig authenticates through an
aws eks get-token exec plugin that reads AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY — the
same two variables. Setting them to the object-store keys makes every later command in that shell
fail with:
error: You must be logged in to the server (Unauthorized)
The error names kubectl, not the variables, which makes it hard to attribute. The in-cluster method
avoids it entirely. If you use the workstation method below, scope the credentials to the one
command with env ... rather than exporting them.
If the object store returns 401, the keys are not the ones this install generated. Keys left
over from a previous install of the same cluster do this, and so does a stale copy in
~/gen0sec-object-store-keys/. Credentials are genuinely validated: wrong keys return
InvalidAccessKeyId, and none at all returns 403.
Workstation method, for reference
This needs no S3 client image in the cluster, which is why it was preferred: on the offline
bundle method an in-cluster image has to be mirrored first, so the bucket comes to depend on a
step that exists only to create the bucket. Use it if that trade-off suits you — but read the
caution above first, because exporting the object-store keys will break kubectl on EKS.
5a. Open a port forward
In a second terminal:
kubectl -n gen0sec-system port-forward svc/rustfs-svc 9000:9000
Leave it running. The output is similar to this:
Forwarding from 127.0.0.1:9000 -> 9000
5b. Create the bucket
Back in your first terminal, use the two keys you generated in step 2. Scope them to the one
command with env rather than exporting them, so they cannot clobber the variables your
kubeconfig's exec plugin reads:
env AWS_ACCESS_KEY_ID="$(cat ~/gen0sec-object-store-keys/RUSTFS_ACCESS_KEY)" \
AWS_SECRET_ACCESS_KEY="$(cat ~/gen0sec-object-store-keys/RUSTFS_SECRET_KEY)" \
AWS_REGION=us-east-1 AWS_EC2_METADATA_DISABLED=true \
aws --endpoint-url http://localhost:9000 s3 mb s3://platform-data
The output is similar to this:
make_bucket: platform-data
BucketAlreadyOwnedByYou is also a pass.
With the MinIO client instead:
mc alias set g0s http://localhost:9000 \
"$(cat ~/gen0sec-object-store-keys/RUSTFS_ACCESS_KEY)" \
"$(cat ~/gen0sec-object-store-keys/RUSTFS_SECRET_KEY)"
mc mb g0s/platform-data
mc ls g0s
5c. Verify it exists
env AWS_ACCESS_KEY_ID="$(cat ~/gen0sec-object-store-keys/RUSTFS_ACCESS_KEY)" \
AWS_SECRET_ACCESS_KEY="$(cat ~/gen0sec-object-store-keys/RUSTFS_SECRET_KEY)" \
AWS_REGION=us-east-1 AWS_EC2_METADATA_DISABLED=true \
aws --endpoint-url http://localhost:9000 s3 ls
The output is similar to this:
2026-09-01 14:02:11 platform-data
An empty result means the bucket was not created, and the services in step 7 will fail with
500 NoSuchBucket. Do not continue.
Then stop the port forward with Ctrl-C in the second terminal.
6. Create the application secrets
$KIT/scripts/make-db-secrets.sh $B/values/gen0sec-platform-values-onprem.yaml
This composes around twenty secrets from the passwords the Postgres operator generated in step 4. That is why it cannot run earlier.
Each service gets its own database role through the connection pooler. The migration job gets a
superuser connection direct to the primary, because it runs CREATE EXTENSION.
make-db-secrets.sh is fine for evaluation. For production, have your secret manager produce
secrets of the same shape. Every name and key is listed in
Secrets.
7. Install the platform
helm install g0s $B/charts/gen0sec-platform-*.tgz \
-n gen0sec \
-f $B/values/gen0sec-platform-values-onprem.yaml \
--set global.imageTag=$VERSION \
--timeout 20m
The chart already points at gen0sec-download-proxy by default. You created that secret in step 2, and
if it is missing the relay pod fails to start with CreateContainerConfigError rather than coming up
half-configured.
Schema migrations run first, as a pre-install hook. If they fail, Helm aborts and nothing is deployed. That is deliberate: a partially migrated database is worse than no install.
Check the migration
kubectl -n gen0sec get job g0s-db-migrate
The output is similar to this:
Error from server (NotFound): jobs.batch "g0s-db-migrate" not found
NotFound is the success case. The job is a Helm hook with
hook-delete-policy: hook-succeeded, so Helm deletes it the moment it passes. If the job is still
there, it may have failed:
kubectl -n gen0sec logs job/g0s-db-migrate
Wait for the pods
kubectl -n gen0sec wait --for=condition=Ready pod --all --timeout=10m
A pod in CrashLoopBackOff still reports status.phase: Running. A --field-selector on phase
reports a broken install as clean.
Both are set in values-onprem.yaml and both point at the in-cluster object store. Check them if
anything cannot read its data.
| Key | Must be |
|---|---|
global.commonEnv.S3_ENDPOINT | http://rustfs-svc.gen0sec-system.svc.cluster.local:9000 |
global.commonEnv.ARXIGNIS_DATA_URL | The same endpoint, plus the bucket: .../platform-data |
ARXIGNIS_DATA_URL is where in-cluster services read their datasets from. It points at your object
store, not at Gen0Sec. The bucket name at the end of it must match the bucket you created in step
5. If you used a different name there, change it here too.
The Service is rustfs-svc, with the -svc suffix. That suffix is the single most common cause of
services that come up and then cannot read anything.
If it fails
| Symptom | Start here |
|---|---|
ImagePullBackOff | ImagePullBackOff |
Install aborts at g0s-db-migrate | Migration failures |
postgresql/core never reaches Running | Postgres will not start |
| Kafka broker crash loops | Kafka will not start |