- ClickHouse 24.8 — the canonical events and evaluations analytics store (StatefulSet with 100Gi persistent volume). Required: the server refuses to start without it.
- PostgreSQL 16 — relational/metadata store for organizations, API keys, users, dashboards, saved queries, and auth (StatefulSet with 50Gi persistent volume)
- Redis 7.2 — optional shared cache and rate-limit backend; the server and dashboard degrade gracefully if it is unavailable
- AgentEye Server — Rust API for event ingestion, analytics, and key management (2 replicas)
- AgentEye Dashboard — Next.js web UI (2 replicas)
- AI assistant (agent service) — optional read-only in-dashboard assistant on port 9100; inert until an LLM endpoint is configured
- Traefik (public) — ingress controller for collector traffic, mTLS-protected
- Traefik (dashboard) — ingress controller for the dashboard, VPN/IP-allowlist-only
- cert-manager — TLS certificates and mTLS CA
- Backup CronJob — daily combined dump of PostgreSQL + ClickHouse at 03:00 UTC
- Cert Renewal Monitor — alerts when client certificates are near expiry
Prerequisites
Run each verification command before starting. Every check must pass.| Requirement | Minimum | Verification Command | Expected |
|---|---|---|---|
| Kubernetes cluster | 1.27+ | kubectl version | Server Version >= v1.27 |
| Kustomize (bundled with kubectl) | Kustomize v1.14+ (ships inside kubectl 1.27+) | kubectl kustomize --help | Prints usage text |
| Helm | v3 | helm version | Version:"v3.x.x" |
| cluster-admin RBAC | — | kubectl auth can-i create namespaces | yes |
| Default StorageClass | — | kubectl get storageclass | At least one row marked (default) |
| LoadBalancer support | — | Cloud-dependent (EKS, GKE, AKS all support this by default) | — |
| GitHub PAT | — | echo $AGENTEYE_TOKEN | Non-empty (see enterprise-docs/github-token.md) |
| openssl | — | openssl version | OpenSSL 1.x or 3.x |
| Cloud storage bucket | — | For PostgreSQL + ClickHouse backups (S3, GCS, or Azure Blob) | — |
Run all checks at once
Deployment shape
The ingest endpoint is served on a hostname you control (e.g.ingest.your-company.example). cert-manager requests a publicly-trusted TLS certificate from Let’s Encrypt over HTTP-01, so collectors verify the server cert against the system trust store, with no per-customer CA pinning.
The dashboard endpoint works the same way: it is served on a second hostname you control (e.g. agenteye.your-company.example) pointing at the dashboard Traefik LoadBalancer, and cert-manager issues its Let’s Encrypt certificate through that LoadBalancer. Browsers get a trusted certificate with no warning.
Certificate issuance and renewal validate over HTTP-01, so both LoadBalancers must be reachable from the public internet on port 80. If you need to IP-restrict the dashboard LoadBalancer, coordinate a DNS-01 solver with support first — otherwise renewals fail silently and the certificate expires.
Get the Manifests
AGENTEYE_TOKEN.
Directory structure:
Health monitoring (optional): the server’s readiness probe already reflects Postgres + ClickHouse health, and third-party/robusta/ adds opt-in Kubernetes-native pod-failure alerting to Slack. See enterprise-docs/health-monitoring.md.
Phase 1 — Third-Party Infrastructure (~30 min)
1.1 Install cert-manager
cert-manager manages TLS certificates for HTTPS and the private CA used for mTLS client certificates.Running — cert-manager, cert-manager-cainjector, cert-manager-webhook.
certificates.cert-manager.io, clusterissuers.cert-manager.io, issuers.cert-manager.io.
If it fails: Pods in CrashLoopBackOff usually means CRDs weren’t installed. Re-run with --set crds.install=true. If webhook pods fail readiness, wait 30 seconds and check again — they can take a moment to start.
1.2 Install Traefik — Public Ingest Controller
This Traefik instance handles collector traffic on an external LoadBalancer. It terminates TLS and enforces mTLS (client certificate verification) on the ingest endpoint.Running.
kubectl describe pod -n traefik-public <pod-name> for image pull errors or resource constraints.
1.3 Install Traefik — Dashboard Controller
This Traefik instance serves the dashboard on a dedicated LoadBalancer, restricted by IP allowlist.Two allowlist mechanisms ship for this instance. This guide usesBefore installing, editvalues-dashboard.yaml, which restricts access with the portableservice.loadBalancerSourceRangesfield. A parallelvalues-internal.yamlis also provided for AWS environments that prefer theservice.beta.kubernetes.io/aws-load-balancer-source-rangesannotation instead. Pick one and use it consistently; the steps below assumevalues-dashboard.yaml.
third-party/traefik/values-dashboard.yaml to set the allowed source IPs. The loadBalancerSourceRanges field controls which IPs can reach the dashboard. By default it is set to 0.0.0.0/0 (all IPs); restrict it to your VPN, office, or known egress IPs.
Whitelist a single IP
Whitelist multiple IPs
Add one entry per IP or CIDR block. A/32 suffix matches a single IPv4 address; a CIDR block (e.g. /24) matches a range. You can mix individual IPs and ranges freely:
- Keep one entry per line and add a short
#comment identifying each IP’s owner or purpose; this is what future operators use to decide whether an entry is still needed. - Always use CIDR notation. A bare IP like
203.0.113.10is rejected by the cloud provider; use203.0.113.10/32. - For IPv6 ranges, use the equivalent
/128(single address) or larger CIDR, e.g.2001:db8::1/128. Not all cloud providers support IPv6 source ranges; check your provider’s LoadBalancer documentation. - The list is an OR: traffic is allowed if the source matches any entry.
helm install below. If the controller is already installed, run helm upgrade with the same flags, or patch the Service at runtime (next section).
Update the whitelist at runtime
You can change the allowed IPs without a Helm upgrade by patching the Service directly. The patch replaces the entire list; always include every IP you want to keep, not just the new one. To replace the list with a new set of IPs:
Runtime patches are not persisted back to values-dashboard.yaml. To keep the change across future Helm upgrades, also update the values file and commit it.
Then install:
Running.
1.4 Wait for LoadBalancers
Both Traefik instances need external IPs before proceeding.EXTERNAL-IP (not <pending>).
If still pending, watch for assignment:
Ctrl+C once the IP appears. IP assignment typically takes 2—5 minutes.
If it fails: <pending> after 10 minutes usually means the cloud provider can’t provision a LoadBalancer. Check: subnet tags (EKS requires kubernetes.io/role/elb), VPC configuration, service quotas, and that the correct internal LB annotation is set for the internal instance.
Phase 2 — Create Secrets (~10 min)
All secrets are created manually before deploying the application. This ensures sensitive values never appear in manifest files.2.1 Create the namespace
Active.
2.2 Image pull secret
This secret authenticates withghcr.io to pull the AgentEye container images. See enterprise-docs/github-token.md for how to generate your PAT.
kubernetes.io/dockerconfigjson.
Test it (deep) — verify the token can actually pull images:
Use the server image tag pinned in your overlay’s kustomization.yaml (currently v0.0.1-beta.48 in both the bundled acme overlay and base deployment). Substitute the tag below for the one you are deploying so this check does not drift across releases:
ok printed in the logs.
If it fails: ErrImagePull or 401 Unauthorized means the PAT is invalid or lacks read:packages scope. Re-check enterprise-docs/github-token.md.
2.3 PostgreSQL credentials
Important: We use-hex(not-base64) to generate the password. Base64 output can contain+,/, and=which break theDATABASE_URLconnection string. See enterprise-docs/troubleshooting.md for details.
Store POSTGRES_PASSWORD in your secrets manager immediately. You will need it if you ever restore from a backup or connect to the database directly.
Test it:
48 (24 hex bytes = 48 characters).
2.4 Admin API key
Store ADMIN_KEY in your secrets manager immediately.
Test it:
2.5 Auth configuration (dashboard login)
The dashboard uses email + OTP for user login. Without this secret the server still starts and theADMIN_KEY API path keeps working, but no user can log in via the UI.
All keys are referenced as optional: true in the base manifest, so partial secrets (or no secret at all) are fine; the server falls back to the documented defaults. Bundling everything into one agenteye-auth secret keeps the auth surface rotatable in a single place.
| Key | Purpose |
|---|---|
ADMIN_EMAIL | Bootstrap admin user. Upserted on every startup with all permissions and protected from deletion/permission edits via the dashboard. Without it, no admin is seeded and first login is impossible. |
ALLOWED_EMAILS | Comma-separated allowlist. Supports exact addresses (user@example.com) and domain wildcards (*@example.com). Without it, no user can log in or be created. |
SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_FROM | SMTP relay for sending OTP codes. If SMTP_HOST is unset, OTP codes are logged to the server’s stdout instead of emailed (useful for first-boot smoke tests). Provide all SMTP keys together for real email delivery. |
SMTP_TLS | One of starttls (default), tls, or none. |
DEFAULT_ORG_NAME, DEFAULT_ORG_SLUG | Optional. Give the built-in default organization a friendly display name and URL slug so it lives at e.g. /acme instead of /default. Applied on first boot only; once you rename the org with agenteye-orgctl org rename (see §7.6) these are ignored. The slug must be 1—40 lowercase alphanumerics with single internal hyphens. Leave both unset to keep the generic default. |
Store the SMTP credentials in your secrets manager.Test it:
2.6 Multi-tenant org isolation key (optional)
Skip this for a single-tenant deployment; the server runs on a built-in dev default and serves the onedefault org fine. Before you create a second organization, set a strong, stable ORG_CH_SECRET: each org’s ClickHouse password is derived as HMAC(ORG_CH_SECRET, org_id), so the publicly-known dev default would yield publicly-derivable per-org credentials. The agenteye-orgctl org create command (see §7.6 Provision organizations) refuses to run while the server is still on the built-in dev default.
secretKeyRef, so a single-tenant cluster that never creates it still boots normally. Keep the value stable and identical across all replicas; rotating it invalidates every org’s derived ClickHouse password until the boot-time reconcile re-provisions the users (a rolling restart with the value consistent everywhere heals it). See deploy/base/server/secret.example.yaml.
Store ORG_CH_SECRET in your secrets manager and don’t rotate it casually.
2.7 Verify all secrets
agenteye-admin-key, agenteye-auth, agenteye-image-pull, agenteye-postgres) must be present before continuing. agenteye-org-ch-secret is only required for multi-tenant deployments (see §2.6).
Phase 3 — Deploy the Application (~5 min)
3.1 Configure the public hostnames
cert-manager needs the ingest and dashboard hostnames before it can request their Let’s Encrypt certificates. Copy the template and set both:domain.env is gitignored; it stays local to each deployment. The kustomize build fails loudly if either key is missing.
DNS must resolve first. You don’t have to point DNS at the LBs yet (they don’t exist until Phase 1.2 is complete), but ACME issuance in step 3.2 will retry until each hostname resolves to its LoadBalancer. You can either set DNS now (using the LB hostnames captured in Phase 1.4) or proceed and add the records in Phase 4.
3.2 Apply manifests
Apply the base directly for a fresh install, or an overlay if you’ve cut one for this environment (overlays just pin image tags, env vars, and resource limits; they inherit the base’s certs and routing):3.3 Wait for pods
agent (AI assistant) and redis pods come up alongside them; the assistant stays inert until you supply its LLM endpoint (see enterprise-docs/assistant.md), and Redis is a best-effort cache, so neither needs to be Ready for the platform to serve traffic.
Test it:
agent and redis pods also appear and reach Running):
| Pod Status | Likely Cause | Debug Command |
|---|---|---|
ImagePullBackOff | Bad image pull secret or PAT | kubectl describe pod <name> -n agenteye |
CrashLoopBackOff | Bad environment variables (e.g. DATABASE_URL) | kubectl logs <name> -n agenteye |
Pending | Insufficient CPU/memory or no nodes | kubectl describe pod <name> -n agenteye (check Events) |
3.4 Verify storage
Bound:
| PVC | Capacity | Backs |
|---|---|---|
postgres-data-postgres-0 | 50Gi | PostgreSQL relational/metadata store |
clickhouse-data-clickhouse-0 | 100Gi | ClickHouse events + evaluations analytics store |
redis-data-redis-0 PVC (1Gi) also appears for the optional cache.
If it fails: Pending means no StorageClass can provision the volume. Check kubectl get storageclass and ensure a default exists. For production, overlay the ClickHouse volume onto a fast SSD StorageClass (e.g. gp3 on AWS, pd-ssd on GCP); compaction throughput suffers on slow disks.
3.5 Verify certificates
Ready: True:
| Name | Issuer | Purpose |
|---|---|---|
mtls-ca | selfsigned | Private CA for issuing mTLS client certs (10-year validity) |
ingest-tls | letsencrypt-prod | Public TLS certificate for the ingest endpoint (90-day, auto-renewed) |
dashboard-tls | letsencrypt-prod | Public TLS certificate for the dashboard (90-day, auto-renewed) |
ingest-tls or dashboard-tls is not Ready:
kubectl describe certificate <name> -n agenteye and read the Events. The common causes:
- DNS not yet pointing at the LB. Let’s Encrypt resolves the hostname and hits port 80 to validate —
INGEST_DOMAINmust resolve to the public LB,DASHBOARD_DOMAINto the dashboard LB. Until the CNAME/Alias propagates, the order stayspending. Once DNS is correct, cert-manager retries automatically (no need to delete the Certificate). - Hostname not substituted. If
dnsNamesstill readsINGEST_DOMAIN_PLACEHOLDER/DASHBOARD_DOMAIN_PLACEHOLDER, you skipped step 3.1 — createbase/certificates/domain.envand re-apply. - Dashboard Traefik can’t serve the challenge (
dashboard-tlsonly). The dashboard Traefik instance must be installed with the bundled values file (Phase 1.2), which enables the scoped Ingress provider that serves cert-manager’s HTTP-01 solver. An instance installed without it leaves the challenge unroutable and the orderpendingforever.
mtls-ca is not Ready: cert-manager itself is unhealthy. Re-check the cert-manager pods from step 1.1.
3.6 Verify CronJobs
| Name | Schedule | Purpose |
|---|---|---|
agenteye-backup | 0 3 * * * | Daily Postgres + ClickHouse backup at 03:00 UTC |
cert-renewal-check | 0 3,15 * * * | Certificate expiry alerts at 03:00 and 15:00 UTC |
3.7 Verify server started correctly
POSTGRES_PASSWORD containing URL-unsafe characters that break the DATABASE_URL. See enterprise-docs/troubleshooting.md.
3.8 Verify dashboard connected to server
Ready in the output with no ECONNREFUSED or similar errors.
If it fails: Check that the server Service exists (kubectl get svc server -n agenteye) and that AGENTEYE_SERVER_URL is set to http://server:8080 in the dashboard deployment.
Phase 4 — Network Access (~5 min)
4.1 Retrieve LoadBalancer addresses
On AWS EKS, LoadBalancers return a hostname instead of an IP. ReplaceTest it:.ipwith.hostnamein the commands above.
4.2 Point DNS at the LoadBalancers
Create DNS records so the hostnames frombase/certificates/domain.env resolve to their LoadBalancers — INGEST_DOMAIN to the public Traefik LB, DASHBOARD_DOMAIN to the dashboard Traefik LB:
- AWS Route 53:
Arecord withAlias = Yes, target = the LB hostname. Don’t use plain A → IP; ELB IPs rotate. - Any other provider:
CNAMEfrom the hostname to the LB hostname.
$PUBLIC_IP and $INTERNAL_IP respectively (or, on EKS, resolve to the same *.elb.amazonaws.com hostnames).
Once DNS resolves, cert-manager finishes the pending ACME orders from Phase 3.5 within a minute. Re-run kubectl get certificates -n agenteye until both ingest-tls and dashboard-tls show Ready: True.
4.3 Reach the ingest endpoint
The public ingest endpoint enforces mutual TLS, so every request (including/health) must present a client certificate. You issue your first client certificate in Phase 5; if you have one already, verify reachability now:
{"status":"ok"}. No -k is needed — the server certificate chains to a public CA for INGEST_DOMAIN, so it validates against the system trust store. Reach the ingest endpoint by its INGEST_DOMAIN hostname (which matches the issued certificate), not by the raw LoadBalancer IP/hostname.
The dashboard endpoint is served on DASHBOARD_DOMAIN with a publicly-trusted certificate and is not behind mTLS, so no -k and no client certificate are needed:
DASHBOARD_DOMAIN, so the raw address shows a certificate-name mismatch.
If it fails: If curl hangs, check that the LB is reachable from your machine (VPN, security groups, firewall rules). A certificate required handshake error on the ingest hostname means no client certificate was presented; complete Phase 5 first. A TLS validation error on the ingest hostname means the server cert hasn’t finished issuing; go back to Phase 3.5 and resolve the issue there.
Phase 5 — Issue mTLS Client Certificates (~10 min per cluster)
Collectors authenticate with two factors: a client certificate (transport layer, proves the request comes from an authorized cluster) and an API key (application layer, proves the request is from a collector withevents:add permission). A leaked key is useless without the cert; a stolen cert is useless without a valid key.
5.1 Issue a certificate
Each cluster running collectors needs its own client certificate. From the manifests directory:<cluster-name> with a meaningful identifier (e.g. us-east-1-prod, staging).
Test it: The script prints ==> Done! and lists the output files.
Ready: True.
Output files in issued/<cluster-name>/:
| File | Purpose |
|---|---|
client.crt | Client certificate (90-day validity) |
client.key | Client private key |
ca.crt | CA certificate for server verification |
collector-mtls-secret.yaml | Ready-to-apply Kubernetes Secret for the collector cluster |
5.1b Alternative delivery: AWS Secrets Manager
If the consumer of the cert is a Kubernetes Pod that needsclient.crt and client.key on disk — the typical case when you run the agenteye-collector as a sidecar in your application pod — push the cert bundle into AWS Secrets Manager. The application pod then mounts it via the Secrets Store CSI Driver with IRSA, and certificate rotation is fully hands-off.
PutSecretValue on the same secret, so the ARN and name stay stable. The CSI Driver picks up the new version on its next rotation poll and rewrites the files inside the pod.
Prerequisites:
awsCLI v2 authenticated to your AWS account.jqinstalled.AWS_REGIONenvironment variable set.- IAM permissions on your caller identity (scope
Resourcetoarn:aws:secretsmanager:<region>:<account>:secret:agenteye/mtls-client/*):secretsmanager:CreateSecretsecretsmanager:DescribeSecretsecretsmanager:PutSecretValuesecretsmanager:TagResource
| Step | Action |
|---|---|
| 1 | Issues / re-extracts the cert via cert-manager (same as default mode). |
| 2 | Calls DescribeSecret on agenteye/mtls-client/<cluster-name> to decide create-vs-update. |
| 3 | On first run: CreateSecret with a three-key JSON payload (client.crt, client.key, ca.crt), tagged AgentEyeCluster=<cluster-name>. On subsequent runs: PutSecretValue to publish a new version; tag refreshed via TagResource. |
| 4 | Deletes issued/<cluster-name>/ only after a successful upload. On any failure, the directory is preserved so you can retry. |
aws secretsmanager restore-secret --secret-id agenteye/mtls-client/<cluster-name> before retrying.
For full pod wiring (SecretProviderClass, IRSA setup, rotation behavior, troubleshooting) see enterprise-docs/single-pod-deployment.md.
5.2 Verify the certificate works
Test the issued certificate against the mTLS ingress:{"status":"ok"}
If it fails:
| Error | Cause | Fix |
|---|---|---|
certificate required | Cert not being presented | Check file paths in the curl command |
bad certificate | CA mismatch | Verify mtls-ca-issuer issued the cert: kubectl describe certificate mtls-client-<name> -n agenteye |
connection refused | Wrong hostname or LB not reachable | Check /etc/hosts or DNS |
5.3 Deliver to the collector cluster
Sendcollector-mtls-secret.yaml to the team operating the collector cluster. They apply it:
client.crt, client.key, ca.crt).
5.4 Certificate lifecycle
| Property | Value |
|---|---|
| Client cert validity | 90 days |
| Auto-renewal | cert-manager renews 15 days before expiry |
| CA validity | 10 years |
| Expiry alerts | CronJob alerts 30 days before expiry (Phase 6) |
issue-client-cert.sh and re-apply collector-mtls-secret.yaml before the old cert expires.
If you are using --save-to aws-secrets-manager (see § 5.1b), re-run the same command. The script calls PutSecretValue on the same secret; pods mounting the secret via the Secrets Store CSI Driver pick up the new version on their next rotation poll (default: every hour), with no pod restart required.
5.5 Revoke a certificate
To immediately block a cluster’s collector access:curl command from step 5.2 now fails with a TLS handshake error.
Phase 6 — Certificate Renewal Monitoring (~2 min)
A built-in CronJob runs every 12 hours (03:00 and 15:00 UTC) and checks all client certificates labeledagenteye.io/cert-type=mtls-client. It alerts when any certificate is within 30 days of expiry.
6.1 Enable Slack notifications (optional)
6.2 Test the CronJob
get, list permissions on cert-manager Certificate resources. Verify with: kubectl describe role cert-renewal-check -n agenteye.
Clean up the test job:
Phase 7 — Verify End-to-End
This phase confirms the entire pipeline works: health check, key creation, event ingestion, and dashboard display.Note: The examples below reach the ingest endpoint by its raw LoadBalancer address (${PUBLIC_IP}) for convenience, which is why they pass-k; the server certificate is bound toINGEST_DOMAIN, not to the LB IP, so the hostname check is skipped. The ingest endpoint enforces mutual TLS on every path, so every call must also present a client certificate (--cert/--key). To validate the public certificate as well, targethttps://ingest.your-company.example/...instead of${PUBLIC_IP}and drop the-k.
7.1 Health check
{"status":"ok"} with HTTP 200.
7.2 Create scoped collector keys
The admin key is for bootstrap and management. Create dedicatedevents:add keys for collectors:
"id", "name": "prod-collector", "permissions": ["events:add"], "created_at".
Test it: Verify the key appears in the key list:
prod-collector appears in the response.
See enterprise-docs/api-keys.md for the full key management reference.
7.3 Ingest a test event
{"accepted":1,"skipped":0} with HTTP 200.
If it fails:
| HTTP Status | Cause |
|---|---|
| 401 | Invalid or missing API key |
| 403 | Key lacks events:add permission |
| TLS handshake error | Client certificate issue — see Phase 5 troubleshooting |
7.4 Verify event appears in dashboard
Openhttps://agenteye.your-company.example (your DASHBOARD_DOMAIN) in a browser. The certificate is publicly trusted, so there is no warning.
If the dashboard LoadBalancer is restricted by IP allowlist and you can’t connect, verify your IP is allowed:Test it: The smoke-test event should appear in the event list with sessionKeep in mind that Let’s Encrypt renews the dashboard certificate over HTTP-01 on port 80, and source ranges apply to the whole LoadBalancer — before restricting it to corporate ranges, coordinate a DNS-01 solver with support or renewals fail silently.
test and agent smoke-test.
If it fails: Check dashboard logs (kubectl logs -n agenteye -l app=dashboard --tail=50). Verify AGENTEYE_SERVER_URL and AGENTEYE_API_KEY are set correctly.
7.5 Test the backup CronJob
Backup created: agenteye-YYYYMMDD-HHMMSS.tar.gz (NNN) in the logs; the archive bundles the Postgres dump and the ClickHouse tables.
The S3 upload step ships wired into the CronJob and runs wheneverClean up:BACKUP_BUCKETis set (the base ships a default bucket value). It is skipped only whenBACKUP_BUCKETis empty or literallyPLACEHOLDER. Point it at your own bucket and grant theagenteye-backupServiceAccount write access before relying on it (see Backups section below).
7.6 Provision organizations (multi-tenant)
Skip this for a single-tenant deployment; all data lives in the built-indefault org and nothing here is required.
If you are running multiple isolated tenants, organizations and their memberships are created with the agenteye-orgctl CLI. It ships inside the server image (alongside agenteye-server) and you run it inside the existing server Deployment with kubectl exec; there is no separate pod, Job, or Deployment, and no HTTP API or dashboard button for tenant lifecycle. Running it in the server pod means it reuses the pod’s DATABASE_URL, CLICKHOUSE_URL, and the ORG_CH_SECRET from §2.6.
Prerequisite: complete §2.6 first.Create an org and add its first admin:org createrefuses to run while the server is still on the built-in devORG_CH_SECRET, and the per-org ClickHouse user it provisions depends on that secret being strong and stable.
/acme/...).
Other commands (run the same kubectl -n agenteye exec deploy/server -- agenteye-orgctl … way):
| Command | What it does |
|---|---|
org list | List organizations and their state. |
org rename --slug <slug> --name <new name> | Rename an org (slug unchanged). |
org delete --slug <slug> | Soft-delete + drop the org’s ClickHouse user; data retained. |
org purge --slug <slug> | Irreversible data wipe; org must be deleted first; never the default org. |
member list --org <slug> | List members and their permissions. |
member update --org <slug> --email <email> [--set ...] [--add ...] [--remove ...] | Change a member’s permissions. |
member remove --org <slug> --email <email> | Remove a member from the org. |
admin, standard, and read-only. Per-org API keys are still minted in the dashboard/API by org members (§7.2 shows the keys API); only the org + member lifecycle is operator-only. Full reference and a worked example: enterprise-docs/tenant-management.md.
Post-Deployment Checklist
Use this checklist to confirm everything is working. Every item should be checked before handing off to collectors.- All pods
Runninginagenteyenamespace - PostgreSQL PVC bound (50Gi) and ClickHouse PVC bound (100Gi)
- All 3 certificates
Ready: True - Both LoadBalancer IPs assigned
- DNS or
/etc/hostsconfigured and resolving -
/healthreturns HTTP 200 - mTLS cert test passes (
curlwith client cert to/health) - Scoped collector key created and tested
- Test event ingested (
accepted: 1) - Event visible in dashboard
- Client certificates issued for each collector cluster
- Backup CronJob tested manually
- Cert renewal CronJob tested manually
- Slack webhook for cert alerts configured (optional)
- Backup bucket configured in overlay (see below)
- Admin key and Postgres password stored in secrets manager
Backups
A singleagenteye-backup CronJob runs daily at 03:00 UTC. It dumps both stores: PostgreSQL (relational state) and ClickHouse (the events + evaluations analytics tables), into one compressed archive in the pod, then uploads it to the object storage you wire in your overlay.
Each run produces one object, agenteye-<timestamp>.tar.gz, which unpacks to:
agent_sessions, the analytics.* aliases) and row policies on startup, so those tables are the complete picture.
Configure cloud upload
The backup CronJob ships with the S3 upload step (aws s3 cp) already wired in, and the base sets a default BACKUP_BUCKET you must override with your own bucket.
On AWS: you only set BACKUP_BUCKET to your bucket and grant the agenteye-backup ServiceAccount write access via IRSA — no script change needed.
On GCP / Azure: you must replace the shipped aws s3 cp line in the CronJob script with the matching command below — do not just add yours, because the leftover aws s3 cp runs under set -eu and fails the job.
| Cloud | Upload Command |
|---|---|
| AWS S3 | aws s3 cp /tmp/${FILENAME} s3://${BACKUP_BUCKET}/${FILENAME} (ships by default) |
| GCP Cloud Storage | gsutil cp /tmp/${FILENAME} gs://${BACKUP_BUCKET}/${FILENAME} |
| Azure Blob | az storage blob upload -f /tmp/${FILENAME} -c backups -n ${FILENAME} |
agenteye-backup ServiceAccount needs IAM permissions to write to the bucket. On AWS use IRSA; on GKE Workload Identity; on AKS Pod Identity. Annotate the ServiceAccount in your overlay accordingly.
The dump is buffered in a 20 GiB emptyDir at /tmp; the ClickHouse events dump dominates its size. If your data is larger, raise the volume’s sizeLimit in your overlay.
Manual backup
Restore from backup
-
Download the archive from your bucket and unpack it:
-
Scale the server down to stop writes during the restore:
-
Restore PostgreSQL:
-
Restore ClickHouse. The schema already exists (the server creates it on startup; on a bare cluster, apply the
.sqlDDL files first). Re-inserting is safe;ReplacingMergeTreecollapses duplicates on merge: -
Scale the server back up:
-
Test it: hit
/healthand confirm the dashboard shows historical events and evaluations.
Upgrading
-
Pull the latest manifests:
-
Check release notes for breaking changes or new environment variables.
Upgrading from a pre-
v0.0.1-beta.6deploy: dashboard login now uses email + OTP and reads its config from a newagenteye-authsecret. Create it per §2.5 before applying, or no user will be able to log in via the UI. API-key traffic is unaffected either way. -
Update image tags in your overlay’s
kustomization.yamlif applicable: -
Take a pre-upgrade backup:
-
Apply:
-
Test it — monitor rollout:
-
Test it — health check:
Expected:
{"status":"ok"} - Test it — dashboard: Open in browser, verify it loads and shows existing events.
Rollback
If something is wrong after upgrading:/health returns 200, dashboard loads and shows events.
Creating Overlays for New Environments
To deploy to a new cluster, create a new overlay by copying the existingacme overlay as a template:
overlays/<new-env>/:
| File | What to Change |
|---|---|
kustomization.yaml | Image tags (newTag values) |
patches/server-env.yaml | DATABASE_URL sslmode if needed |
patches/resource-limits.yaml | CPU and memory requests/limits |
patches/cert-renewal-env.yaml | Slack webhook URL |
Architecture Reference
Kubernetes resources created
| Kind | Name | Namespace | Purpose |
|---|---|---|---|
| Namespace | agenteye | — | All AgentEye resources |
| StatefulSet | clickhouse | agenteye | ClickHouse 24.8 with 100Gi PVC — canonical events + evaluations store |
| StatefulSet | postgres | agenteye | PostgreSQL 16 with 50Gi PVC — relational/metadata store |
| StatefulSet | redis | agenteye | Redis 7.2 with 1Gi PVC — optional shared cache |
| Deployment | server | agenteye | Rust/Axum API (2 replicas, pod anti-affinity) |
| Deployment | dashboard | agenteye | Next.js UI (2 replicas) |
| Deployment | agent | agenteye | Optional AI assistant (1 replica); inert until an LLM endpoint is configured |
| Service | clickhouse | agenteye | ClusterIP, port 8123 (HTTP) |
| Service | postgres | agenteye | ClusterIP, port 5432 |
| Service | redis | agenteye | ClusterIP, port 6379 |
| Service | server | agenteye | ClusterIP, port 8080 |
| Service | dashboard | agenteye | ClusterIP, port 3000 |
| Service | agent | agenteye | ClusterIP, port 9100 (internal-only) |
| PodDisruptionBudget | clickhouse | agenteye | Keeps at least one ClickHouse pod available during disruptions |
| NetworkPolicy | agent | agenteye | Restricts ingress to the AI assistant service |
| Certificate | mtls-ca | agenteye | Self-signed CA (10-year, ECDSA P-256) |
| Issuer | mtls-ca-issuer | agenteye | Issues client certs from the CA |
| Certificate | ingest-tls | agenteye | Server TLS for ingest endpoint (90-day, Let’s Encrypt) |
| Certificate | dashboard-tls | agenteye | Server TLS for dashboard (90-day, Let’s Encrypt) |
| IngressRoute | ingest | agenteye | Routes /events, /health, /keys via traefik-public with mTLS |
| IngressRoute | dashboard | agenteye | Routes / via traefik-dashboard (no mTLS) |
| TLSOption | mtls-ingest | agenteye | Requires and verifies client certificates |
| CronJob | agenteye-backup | agenteye | Daily Postgres + ClickHouse backup at 03:00 UTC |
| CronJob | cert-renewal-check | agenteye | Certificate expiry alerts, twice daily |
| ServiceAccount | agenteye-backup | agenteye | IAM-annotatable SA for backup bucket access |
| ServiceAccount | cert-renewal-check | agenteye | SA for cert-manager Certificate read access |
| Role | cert-renewal-check | agenteye | get, list on cert-manager Certificates |
| RoleBinding | cert-renewal-check | agenteye | Binds role to ServiceAccount |
PostgreSQL sizing and tuning
The base manifests ship a single-replica PostgreSQL 16 StatefulSet with a tunedpostgresql.conf mounted from a ConfigMap, so the dashboard’s hot read paths get a real buffer cache and work_mem budget out of the box rather than the image defaults.
Default resources (acme overlay):
| Resource | Request | Limit |
|---|---|---|
| CPU | 1 | 4 |
| Memory | 4 Gi | 8 Gi |
| Storage | 50 Gi PVC (RWO, default StorageClass) | — |
postgresql.conf highlights (full file in postgres/configmap.yaml):
| Setting | Value | Rationale |
|---|---|---|
shared_buffers | 2GB | ~25% of the 8 Gi limit |
effective_cache_size | 6GB | ~75% of the limit |
work_mem | 32MB | Headroom for dashboard sorts (events / evaluations history) |
maintenance_work_mem | 512MB | Faster CREATE INDEX, VACUUM |
max_connections | 100 | Server pool (10 × 2 replicas) + readonly pool (4) + headroom |
random_page_cost | 1.1 | SSD-backed PVC |
jit | off | OLTP-shaped workload |
shared_preload_libraries | pg_stat_statements | Per-query statistics |
log_min_duration_statement | 250ms | Surface slow queries in pod logs |
postgres-config ConfigMap in your overlay. Keep shared_buffers around 25% of the memory limit and effective_cache_size around 75%. After editing, restart the pod (kubectl -n agenteye rollout restart statefulset/postgres) for the new postgresql.conf to take effect.
Inspect slow queries once the pod is running. pg_stat_statements aggregates per-query timing, so this is the fastest way to find what is loading the database. Its views live in the extensions schema (not public) so they stay hidden from the read-only role that backs the dashboard’s ad-hoc query workbench:
Environment variables
Server:| Variable | Source | Value |
|---|---|---|
DATABASE_URL | Constructed from agenteye-postgres secret | Built at deploy time from secret values (see server/deployment.yaml). Points at the relational/metadata store. |
CLICKHOUSE_URL | Deployment manifest | http://clickhouse:8123 — required; the canonical events + evaluations store. The server refuses to start without it. |
CLICKHOUSE_DATABASE | Deployment manifest | agenteye |
REDIS_URL | Deployment manifest | redis://redis:6379/0 — optional cache and rate-limit backend. If Redis is unreachable, auth and OTP fall back to PostgreSQL (graceful degradation). |
ADMIN_KEY | agenteye-admin-key secret | Bootstrap admin credential |
AGENT_API_KEY | agenteye-agent secret (optional) | Seeds the protected dashboard-assistant key the AI assistant’s agent service uses (the same value the agent reads as AGENTEYE_API_KEY). Unset means the key isn’t seeded and the assistant stays off. See enterprise-docs/assistant.md. |
AGENTEYE_AGENT_URL | Deployment manifest | http://agent:9100 — where the server reaches the agent service to run the audit’s agentic investigation (the same connection the dashboard uses). Unset ⇒ audits run policy-only. See enterprise-docs/assistant.md. |
AGENTEYE_AGENT_TOKEN | agenteye-agent secret (optional) | Shared secret the server presents to the agent for audit investigation calls; must match the agent’s AGENTEYE_AGENT_TOKEN. Unset ⇒ audits run policy-only. |
ADMIN_EMAIL | agenteye-auth secret (optional) | Bootstrap admin user email, upserted as protected on every startup |
ALLOWED_EMAILS | agenteye-auth secret (optional) | Comma-separated allowlist for user creation/login; supports *@domain wildcards |
SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_FROM, SMTP_TLS | agenteye-auth secret (optional) | SMTP relay for OTP delivery; if SMTP_HOST is unset, OTPs are logged to stdout |
DEFAULT_ORG_NAME, DEFAULT_ORG_SLUG | agenteye-auth secret (optional) | First-boot-only display name and URL slug for the built-in default org; ignored after an org rename. Slug must be 1—40 lowercase alphanumerics with single internal hyphens. See §2.5. |
EVALUATOR_ENDPOINT, EVALUATOR_TOKEN | agenteye-evaluator secret (optional) | Wire the evaluation pipeline. With the secret absent the pipeline is a no-op. See enterprise-docs/evaluation-suite.md. |
SESSION_TTL_SECS | Default | 86400 (24 h) |
OTP_TTL_SECS | Default | 600 (10 min) |
ORG_CH_SECRET | agenteye-org-ch-secret secret (optional; required before a 2nd org) | HMAC key from which each organization’s per-tenant ClickHouse password is derived. Single-tenant clusters run on the built-in dev default; set a strong, stable value in your own secret before provisioning a second organization (see §2.6), and keep it identical across all server replicas; rotating it orphans every org’s ClickHouse user until the next startup re-provisions them. |
RUST_LOG | Deployment manifest | info |
LISTEN_ADDR | Default | 0.0.0.0:8080 |
MAX_BODY_BYTES | Default | 134217728 (128 MB) |
Multi-tenant ClickHouse. Per-org isolation requires SQL access management on ClickHouse; the bundledDashboard:deploy/base/clickhouse/config enables it (access management +users_without_row_policies_can_read_rows=false) so the server can create one read-only ClickHouse user and row policy per organization. If you replace that config, carry those settings over.
| Variable | Source | Value |
|---|---|---|
AGENTEYE_SERVER_URL | Deployment manifest | http://server:8080 |
AGENTEYE_API_KEY | agenteye-admin-key secret | Same as ADMIN_KEY |
AGENTEYE_AGENT_URL | Deployment manifest | http://agent:9100 — where the dashboard reaches the optional AI assistant |
AGENTEYE_AGENT_TOKEN | agenteye-agent secret (optional) | Shared secret the dashboard presents to the AI assistant; the assistant stays inert until its LLM endpoint is configured. See enterprise-docs/assistant.md. |
REDIS_URL | Deployment manifest | redis://redis:6379/0 — optional; caches validateSession() results across replicas and shares the Next.js fetch cache. Unset => every authed request falls through to the server (correct, just slower). |
HOSTNAME | Deployment manifest | 0.0.0.0 |
Traffic flow
Troubleshooting
The most useful commands for deployment-specific issues:Related Documentation
| Guide | Description |
|---|---|
| Managed Deployment | Setup guide for managed deployment on your Kubernetes cluster |
| Getting Started | End-to-end walkthrough with Docker Compose |
| Deployment | Docker-based deployment, env vars, configuration |
| Tenant Management | Provision organizations & members with the operator-only agenteye-orgctl CLI |
| GitHub Token Setup | Generate your GitHub PAT to access artifacts |
| Collector Installation | All collector install methods |
| Python SDK | Full SDK API reference |
| API Keys | Creating and managing API keys |
| Troubleshooting | Common issues and fixes |
Support
Emailsupport@exosphere.host for help with deployment.
