Skip to main content

Install

Before you start

  • Prepare the artifacts complete
  • $KIT, $B, $REGISTRY, $REGISTRY_USER, $REGISTRY_TOKEN are set in your shell
  • 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

kubectl create namespace gen0sec-system
kubectl create namespace gen0sec

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
done

The object store credentials

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.

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
Why the last one resolves its own kind

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.

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.

If you changed the bucket name

platform-data is the value of global.commonEnv.S3_BUCKET. If you overrode it, use your name instead.

The port-forward method does not currently work

Earlier revisions of this page created the bucket from your workstation over kubectl port-forward. That path returns 401 Unauthorized against the object store in v0.1.0-rc.9, with correct credentials, and the same is true of the mc alternative. It is not a credentials problem: deliberately wrong keys produce the identical error, and the same amazon/aws-cli:2.15.0 succeeds from inside the cluster against the same object store.

There is a second trap on managed Kubernetes. The old instructions had you export AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY for the object store — but on EKS the kubeconfig authenticates through an aws eks get-token exec plugin that reads those same variables, so exporting the object-store keys breaks kubectl itself in that shell:

error: You must be logged in to the server (Unauthorized)

The error names kubectl, not the environment variables, which makes it hard to attribute. The in-cluster method above avoids both problems, because the credentials stay in the cluster.

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. It is kept here for that reason, but see the caution above before using it.

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.

export AWS_ACCESS_KEY_ID=$(cat ~/gen0sec-object-store-keys/RUSTFS_ACCESS_KEY)
export AWS_SECRET_ACCESS_KEY=$(cat ~/gen0sec-object-store-keys/RUSTFS_SECRET_KEY)
export AWS_REGION=us-east-1
export 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

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.

Scope the credentials to the command rather than exporting them, so they cannot clobber the variables your kubeconfig's exec plugin uses:

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

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.

In production, treat this script's output as a specification

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
Nothing to set for the relay

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
Check readiness, not phase

A pod in CrashLoopBackOff still reports status.phase: Running. A --field-selector on phase reports a broken install as clean.

Two values that must match your cluster

Both are set in values-onprem.yaml and both point at the in-cluster object store. Check them if anything cannot read its data.

KeyMust be
global.commonEnv.S3_ENDPOINThttp://rustfs-svc.gen0sec-system.svc.cluster.local:9000
global.commonEnv.ARXIGNIS_DATA_URLThe 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

SymptomStart here
ImagePullBackOffImagePullBackOff
Install aborts at g0s-db-migrateMigration failures
postgresql/core never reaches RunningPostgres will not start
Kafka broker crash loopsKafka will not start