K7 0.5.0 turns the API into a multi-tenant control plane. Three things changed from 0.4.0: every credential now has a role; namespaces are grouped into tenants that carry quotas, defaults and dedicated nodes; and humans can sign in with OIDC instead of holding a key. Keys moved from /etc/k7/api_keys.json on a node to the kube-system/k7-api-keys Secret, so every API replica sees the same store.
K7 roles are application roles inside k7-api, not Kubernetes RBAC. The k7-api ServiceAccount stays the only Kubernetes actor; nobody gets a Kubernetes identity through K7, and a K7 admin cannot kubectl anything. Protect kubeconfigs and node root separately.
Roles#
| Role | Scope | Can |
|---|---|---|
read-only | one tenant | list/get sandboxes, metrics and snapshots |
user | one tenant | read-only plus create/delete, exec, logs, pause/resume, fork, snapshots |
operator | cluster | read everything, snapshot GC, node storage, node dedication, tenant read; no exec, no keys |
admin | cluster | everything, including API keys, tenants, role bindings and remote install |
One table (PERMISSIONS in src/k7/api/authz.py) maps every route to its roles; a route missing from it answers 500 and fails CI. Checks compose — route role, then tenant, then the key’s namespace/node narrowing — and the first failure is a 403 that names the missing role or scope. k7 whoami / GET /api/v1/auth/whoami shows what a credential resolves to.
Tenants#
A Tenant (tenants.k7.katakate.org/v1alpha1, cluster-scoped) claims namespaces and owns everything a tenant key may do:
- Namespaces are created if missing and labelled
k7.katakate.org/tenant. A tenant key reaches every namespace of its tenant (intersected with its own-nlist), read from the Tenant on every request — a namespace added later works without reissuing the key, and a key whose tenant is deleted reaches nothing. A namespace claimed by two Tenants (only possible by writing CRs by hand) fails every request there with 500TenantConflict. - Dedicated nodes (
spec.nodes, ork7 nodes dedicate): the tenant’s sandboxes land only there (node selector + toleration), and theNoScheduletaint keeps everyone else off. This is what gives tenant isolation on k7d, whose daemon is shared per node — a key’s--nodepin alone restricts that key, not the others. defaultSize(cpu,memory,disk) fills creates without limits. Production mode requires CPU and memory on every sandbox, so set it or make callers passlimits.- Quotas (
sandboxes,vcpu,memory,disk,snapshotStorage) are checked at K7’s two choke points — the sandbox Deployment create and the VolumeSnapshot create — so create, fork, restore, pause-with-snapshot and named snapshots are all covered. The check and the create run under a per-tenant Lease (k7-quota-<tenant>), so concurrent creates on any number of API replicas cannot overshoot; a Lease not acquired in 5 s is 503 and creates nothing. Over-quota is403 QuotaExceededwith the dimension, limit, usage and request, and is audit-logged. Usage is reserved, not used: a paused sandbox keeps its vCPU, memory and disk until deleted. The leader replica writesTenant.status.usageevery 60 s. - Per-VM limits :
maxSandboxSizecaps a request (400 above it).network(egressBps/ingressBps, bits/s) is enforced by the hypervisor — Firecracker token buckets on kfd,tcon the tap for kql — so it works whatever the CNI; a request may lower a cap, never raise it. A backend that cannot enforce a configured limit refuses the create with400 LimitNotEnforceableinstead of running unlimited:diskIOon every backend (no block rate limiter in Kata 3.24 or k7d yet),networkon k7d / k7d-fc, and adiskquota ormaxSandboxSize.diskon k7d / k7d-fc (K7 cannot read a k7d VM’s writable-disk size). - Enforcement is at the K7 API.
kubectl applyby a cluster admin bypasses it. For a single-namespace tenant a KubernetesResourceQuota(k7-tenant-quota) is the backstop; multi-namespace tenants have none, since a ResourceQuota cannot span namespaces. - Migrating from 0.4.0 namespace-scoped keys:
k7 tenant adopt --from-keysproposes one tenant per connected group of key namespaces (dry run),--applycreates them and binds the keys.
API keys#
Tokens are k7_<id>_<secret>; the store holds sha256(secret) (the secret is 256 random bits, so a slow KDF adds nothing) in the kube-system/k7-api-keys Secret. The API never writes the store on authenticate: it serves a snapshot refreshed every 5 s and answers 503 rather than trust one older than 60 s, so revocation reaches every replica within one refresh. Key commands edit the Secret through kubectl (run them on a server node), or go through k7-api with --api using an admin credential (POST /api/v1/keys). Protect etcd (--secrets-encryption on K3s, KMS on managed clusters): anyone with get secrets in kube-system reads the hashes.
Upgrading from 0.4.0: k7 install runs k7 api keys migrate, which copies /etc/k7/api_keys.json into the Secret and gives every pre-role key a role: admin: true → admin, namespace-scoped → user (still narrowed), unscoped → admin flagged unscoped_legacy. Such keys keep working in development mode; re-issue them with a real role before switching to production.
OIDC login#
Install with all five settings (or none; https:// issuers only):
Then k7 login (device flow; --browser for Authorization Code + PKCE on a loopback redirect) caches the tokens in ~/.config/k7/credentials.json (0600) and refreshes them itself; k7 logout deletes the cache and revokes at the IdP. Clients send Authorization: Bearer <JWT>; k7-api validates statelessly — signature against the issuer’s JWKS (cached --oidc-jwks-ttl seconds, default 3600; a failed refresh is 503, never a stale allow), alg RS256/ES256 only, iss, aud, exp (60 s leeway) and sub required, email_verified: true when the username claim is email. k7-api stores no sessions and never sees refresh tokens. Register k7-cli as a public client with the device grant enabled. Per-IdP claim settings (Keycloak, Entra ID, Okta, Auth0, Dex) are in docs/HARDENING.md.
Role bindings#
Groups, usernames or subjects map to roles through ConfigMap k7-role-bindings, read on every request:
A subject in several groups gets the highest role per tenant plus the highest cluster role. An unbound subject gets a 403 that echoes its username and groups, to debug claim shapes. A write that leaves no admin binding while no unexpired admin key exists is refused. A JWT is valid until exp — configure 1 h or shorter tokens at the IdP; deny_subjects is the immediate stop. Keep one break-glass admin key offline: keys keep working during an IdP outage, and k7 generate-api-key with the node kubeconfig always works.
Production and development modes#
--api-mode is chosen at install and never defaulted (chart: api.mode). Every response carries X-K7-Mode; /health and k7_api_info{mode} report it.
production | development | |
|---|---|---|
Keys without a tenant or cluster role (unscoped_legacy, pre-role, namespace-only) | refused at startup (k7-api names each one) and 403 on every route | work as cluster admin |
read-only / user keys | need --tenant | may be unscoped |
| Sandbox CPU and memory limits | required (from limits or the tenant’s defaultSize) | optional |
--api-insecure-http | refused | allowed |
| Metering | --metering-sample-interval required | 15 s default |
--api-remote-install | refused before deploying | allowed |
Switch a development cluster to production by re-running k7 install --api-mode production … once every key has been re-scoped (k7 list-api-keys shows none flagged unscoped_legacy).
Checklist#
- Migrate keys (
k7 api keys migrateruns on install) and re-issue everyunscoped_legacykey with the narrowest real role. - Give machines tenant keys with
--expires-days; give humans OIDC. - Create tenants with
defaultSizeand quotas; dedicate nodes for tenants that must not share a k7d daemon. - Bind groups, not people; keep a break-glass
adminkey offline. - Ship the audit log (k7-api stdout, logger
k7.audit). - Install with
--api-mode production.
Reference: SECURITY.md, docs/HARDENING.md, API: Identity, Tenants.
