Security
The security model as an operator needs to know it.
Harthelm runs code your users write, next to your platform. Its model rests on four boundaries; this page says what each one is made of and what you must provide for it to hold.
1. The console and the workspaces are different sites
The console and the workspace domain are different registrable domains, so code in a workspace cannot set or read the
console's cookies. Every cookie is __Host- prefixed, Secure and HttpOnly, with no Domain. Changes made with the
console's session must carry a CSRF header and the console's origin.
Private workspace URLs are gated by the router: only signed-in members of the team pass. A role change applies to an open workspace session too: the router closes connections that an older role opened.
The browser hosts of managed services (ms-<n>.<workspace domain>, since 1.4.0) are gated the same way. RabbitMQ's
management UI is never public; its host forbids framing and accepts changing requests only from itself and the
console. Its single sign-on uses short-lived tokens (one hour, the tags management and policymaker on the broker's
virtual host only) signed with a key per broker that is derived from the master key; they never appear in a URL, a
log line or an audit row, and for a broker bound to a landscape only the workspace's editors and admins get one. An S3
store's host is public only when a team switches it public: then anonymous clients read the objects of its public
buckets, and everything else needs the store's key.
You provide: two registrable domains (DNS and TLS) and correct
trustedProxies.
2. Teams are isolated from each other and from the platform
Every team has its own namespace with a quota, Pod Security restricted and NetworkPolicies: no traffic to another
team, DNS only to the cluster's DNS, core only on its API and git proxy ports, the internet but no private address and
no SMTP. On Cilium a cluster-wide backstop denies the same even if someone adds a policy in a team namespace.
Workspace pods run as one non-root UID with no Kubernetes API token.
You provide: a CNI that enforces NetworkPolicy with endPort, proven with helm test
(Network policies). Without enforcement this boundary does not
exist, and the doctor fails.
3. Core can only touch what is its own
Core needs broad rights to create team namespaces and their objects. A ValidatingAdmissionPolicy limits its
ServiceAccount to namespaces it created (rg-* with its label), the release namespace, and cluster-scoped objects with
its label. The router can only read EndpointSlices. See Permissions.
Known limit: Kubernetes RBAC cannot narrow a ClusterRole by name or label, so core's ServiceAccount can read Secrets in every namespace. Only core's pod holds that token.
You provide: Kubernetes 1.30 or newer for the guard (1.31 is required anyway), and keep
security.admissionGuard.enabled on.
4. Secrets are sealed
Git tokens, secrets of workspaces and services, uploaded keys and queued invitation links are sealed with AES-256-GCM under the master key. API keys are stored only as hashes. Logs carry names and ids, never values.
Inside the cluster, traffic to core uses TLS with the release's own CA when internal.tls is on (the default with
cert-manager).
You provide: a copy of the master key outside the cluster (Backups and restore), and cert-manager for in-cluster TLS.
What leaves the cluster
The platform downloads nothing at runtime: no telemetry, no update checks. Its own outbound connections go only to what
you configure (networkPolicy.egress): the API server, the identity provider, the Git provider, the mail relay,
container registries and an external database. Narrow each rule to its addresses; the doctor warns about rules that
allow every address.
Server-side requests that users can influence are fenced:
- container registries: core never dials loopback or link-local addresses, and without a
toits registry rule allows only public addresses.containers.allowedRegistriesnarrows the hosts further; - forward services: the router's own address check plus its NetworkPolicy (
router.forward.allowPublic,router.forward.allowCIDRs); - custom domain verification asks fixed public resolvers, never the cluster's DNS.
Harthelm 1.0 neither sends nor receives webhooks.
Strict-Transport-Security
Since 1.5.0 every response of core's public port (the console, /api/v1, /auth/*, the API reference, the probes)
carries Strict-Transport-Security: max-age=31536000, so a browser that has seen the console once refuses plain HTTP
for it for a year. There is no value to set. The header names no includeSubDomains and no preload on purpose: the
policy is per host, and it must never reach the workspace domain, whose wildcard holds the teams' dev hosts, the managed
services' hosts and whatever else you put under that domain. Neither core nor the router sends it for workspace hosts;
the edge and its operator decide there. The in-cluster API port sends none, and a browser ignores the header on a
plain-HTTP response. An operator who wants the policy on other hosts (the identity provider's, the docs) sets it at the
edge.
Accounts
- Sign-in is OIDC; cluster admins come from a group and are recomputed at every sign-in.
- The break-glass admin is optional, rate-limited, audited and announced by mail.
- Console sessions end after 12 hours idle or 7 days.
hart admin auditlists the audit log: sign-ins, API keys, membership and workspace changes.
Hardening checklist
-
helm testpasses, and the doctor'snetwork-policycheck is ok. - Every
networkPolicy.egressrule has ato. -
trustedProxieslists only your ingress pods. -
internal.tlsis on. - The release namespace enforces Pod Security
restricted. - Cloud metadata endpoints are blocked for pods at the node level too (IMDSv2 hop limit 1, Workload Identity).
-
containers.allowedRegistriesnames the registries your teams may use. -
marketplace.browserHosts.allowPublicisfalseunless your teams may serve S3 stores to the internet. - The master key is stored outside the cluster, and a restore was practised.
- The break-glass password is stored where on-call people find it, and
mail.breakGlassToreaches them.