Harthelm Docs

Security

The security model as an operator needs to know it.

Last updated 2026-10-19

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 to its registry rule allows only public addresses. containers.allowedRegistries narrows 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 audit lists the audit log: sign-ins, API keys, membership and workspace changes.

Hardening checklist

  • helm test passes, and the doctor's network-policy check is ok.
  • Every networkPolicy.egress rule has a to.
  • trustedProxies lists only your ingress pods.
  • internal.tls is 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.allowedRegistries names the registries your teams may use.
  • marketplace.browserHosts.allowPublic is false unless 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.breakGlassTo reaches them.

On this page