Install
Before you start
- Prepare the artifacts complete
-
$KIT,$B,$REGISTRY,$REGISTRY_USER,$REGISTRY_TOKENare 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
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.
platform-data is the value of global.commonEnv.S3_BUCKET. If you overrode it, use your name
instead.
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.
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 |