Helm Charts
| Chart | Purpose | Install Command |
|---|---|---|
| itops | Core platform (API + UI + Landing + PostgreSQL) | helm install itops itops/itops |
| itops-agent | K8s operator (per cluster) | helm install itops-agent itops/itops-agent |
| sla-portal | Standalone SLA status page | helm install sla-portal itops/sla-portal |
Installation Values
The platform is fully configurable via Helm values. No hardcoded domains or credentials in the images.
Minimal Production Install
# my-values.yaml
ingress:
hosts:
- host: api.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- api.yourdomain.com
secretName: itops-api-tls
uiIngress:
hosts:
- host: app.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- app.yourdomain.com
secretName: itops-ui-tls
ui:
apiUrl: "https://api.yourdomain.com"
wsHost: "api.yourdomain.com"
env:
ITOPS_SERVER_ENVIRONMENT: "production"
ITOPS_SECURITY_CORS_ALLOWED_ORIGINS: "https://app.yourdomain.com"
secretEnv:
ITOPS_DATABASE_PASSWORD: "strong-random-password"
ITOPS_JWT_SECRET: "random-jwt-secret-min-32-chars"
ITOPS_SECURITY_OPERATOR_API_KEY: "random-api-key-for-agents"
ITOPS_LICENSE_KEY: "eyJhbGciOiJFZERTQSIs..." # JWT license token
secretEnv.ITOPS_LICENSE_KEY. Stored as a Kubernetes Secret and injected as an environment variable. Without a valid license, paid plugins (Ticketing, SLA) are hidden. The token is Ed25519-signed (not HMAC) and validated at every startup.
secretEnv: end up in your Git repo. For production use SealedSecret, External Secrets Operator, or sops-age to encrypt these values before committing. The Helm chart reads env vars at runtime — swap the Helm secretEnv for extraEnvFrom: [secretRef: { name: my-vault-synced-secret }] and you never have to touch raw values in Git.
ITOPS_SECURITY_CORS_ALLOWED_ORIGINS must be set to your UI hostname (e.g. https://app.yourdomain.com). Leaving the dev default (localhost) in production means browsers outside localhost won't be able to call the API.
bitnamilegacy/* image mirror and sets global.security.allowInsecureImages: true (needed because Bitnami moved their free public images in Aug 2025). This is safe for self-hosted deployments pulling from Docker Hub, but for production prefer a managed PostgreSQL (RDS, Cloud SQL, self-managed cluster) — set postgresql.enabled: false and point ITOPS_DATABASE_HOST at it.
Security Hardening (shipped since v4.1.2)
The platform ships with defense-in-depth against a compromised admin:
- SSRF validator on every outbound HTTP (webhooks + workflow
HTTP_REQUESTsteps). URLs are resolved and rejected if they land on loopback (127.0.0.0/8), private RFC1918 (10/8,172.16/12,192.168/16), link-local / cloud-metadata (169.254.0.0/16), or use non-http(s)schemes. The check runs on redirects too, so a302to localhost can't smuggle the request through. - License activation is authenticated with the operator API key (
ITOPS_SECURITY_OPERATOR_API_KEY). Before v4.1.2,POST /api/v1/license/activatewas open to anyone on the network. - Pod
securityContext: runs as non-root (UID 1000), read-only root filesystem,allowPrivilegeEscalation: false, all Linux capabilities dropped, seccompRuntimeDefault. - Dedicated
ServiceAccountwithautomountServiceAccountToken: false— even an RCE can't reach the Kubernetes API. - Egress
NetworkPolicy(on by default): DNS to kube-dns, PostgreSQL to the bundled chart, HTTPS/HTTP to the public internet except RFC1918, loopback, link-local and CGNAT. This is belt-and-braces in case the SSRF validator is ever bypassed. Disable only for debugging:networkPolicy.enabled: false.
What the install looks like with hardening on (GitOps)
The hardened defaults are shipped in the chart — no opt-in needed. A minimal GitOps values file for a production deployment looks like this:
# platform/itops-values.yaml
ingress:
hosts:
- host: api.yourdomain.com
paths: [{ path: /, pathType: Prefix }]
uiIngress:
hosts:
- host: app.yourdomain.com
paths: [{ path: /, pathType: Prefix }]
ui:
apiUrl: "https://api.yourdomain.com"
wsHost: "api.yourdomain.com"
env:
ITOPS_SERVER_ENVIRONMENT: "production"
ITOPS_SECURITY_CORS_ALLOWED_ORIGINS: "https://app.yourdomain.com"
# --- SLA Portal integration (daily report push) ---
# Public URL goes through the internet lane — already allowed.
# In-cluster URL needs allowedEgressNamespaces below.
ITOPS_SLA_PORTAL_URL: "https://sla.yourdomain.com"
# Secrets — in real GitOps, SealedSecret or External Secrets populates these.
# Shown here as plain secretEnv for brevity.
secretEnv:
ITOPS_DATABASE_PASSWORD: "strong-random"
ITOPS_JWT_SECRET: "32+chars"
ITOPS_SECURITY_OPERATOR_API_KEY: "shared-agent-key"
ITOPS_LICENSE_KEY: "eyJhbGciOiJFZERTQSIs..."
ITOPS_SLA_PORTAL_API_KEY: "sla-portal-shared-key"
networkPolicy:
enabled: true
# Leave default unless your SLA Portal lives in a different namespace than "sla-portal"
allowedEgressNamespaces:
- sla-portal
Opening specific targets when the default is too strict
When a webhook needs to reach an in-cluster service (private n8n, internal Jira, managed DB on RFC1918), use these explicit escape hatches — never blanket-allow everything:
env:
# Restrict webhooks to a strict host allowlist (recommended if you have
# ANY in-cluster webhook target)
ITOPS_SECURITY_WEBHOOK_HOST_ALLOWLIST: "n8n.corp.local,hooks.slack.com,api.pagerduty.com"
# Dev-only nuclear option: disable the private-IP block entirely.
# Don't set this in production — it reopens the SSRF hole.
# ITOPS_SECURITY_ALLOW_PRIVATE_WEBHOOKS: "true"
networkPolicy:
extraDatabaseCIDRs:
- 10.20.30.0/24 # managed PostgreSQL subnet
allowedEgressNamespaces:
- sla-portal # keep for SLA Portal daily push
- automation # e.g. an in-cluster n8n namespace
What breaks if you ignore the defaults
- Admin tries to click "Activate License" in the UI — works (JWT path). Admin tries to call the endpoint from a script — needs X-API-Key now.
- Admin creates webhook with URL
http://itops-postgresql:5432/— rejected by the SSRF validator with a clear error message in the webhook history tab. Same forhttp://169.254.169.254/...,file:///etc/passwd, etc. - Workflow template with a
HTTP_REQUESTstep pointing atkubernetes.default.svc— same outcome, step fails with "URL rejected by SSRF policy". - SLA Portal daily push to
http://sla-portal.sla-portal.svc:80/api/v1/report— works becauseallowedEgressNamespaceshassla-portal. If you deploy SLA Portal to a different namespace, update the list. - Container tries to write outside
/var/log/itopsor/tmp— fails becausereadOnlyRootFilesystemis on. Those two paths are emptyDir-mounted for you (SLA reports + scratch).
helm install itops itops/itops -n itops --create-namespace -f my-values.yaml
Default login: admin / Password123! (change after first login)
Istio Support
If your cluster uses Istio instead of nginx Ingress:
istio:
enabled: true
tls:
enabled: true
credentialName: "my-tls-cert"
ingress:
enabled: false # disable nginx Ingress
uiIngress:
enabled: false
External Database
To use an existing PostgreSQL instead of the bundled one:
postgresql:
enabled: false
env:
ITOPS_DATABASE_HOST: "my-postgres.default"
ITOPS_DATABASE_PORT: "5432"
ITOPS_DATABASE_NAME: "itops"
ITOPS_DATABASE_USER: "itops"
secretEnv:
ITOPS_DATABASE_PASSWORD: "my-db-password"
Plugin Management
Plugins (Ticketing, SLA) can be enabled/disabled from the admin UI at /admin/plugins. Disabling a plugin hides its menus and stops background processing, but preserves all existing data.