A tenant = one terraform.tfvars entry + one authentik
group membership. Everything else is provisioned as code or documented
here for the tenant to apply themselves. Their repo is
theirs — the platform never writes to it; this page is what we
hand them instead.
infrastructure/k3s/terraform/terraform.tfvars
— add an entry:
project_runners = {
<slug> = {
project_path = "<Namespace>/<project>"
tags = ["<slug>-ci"]
untagged = true # run their Auto DevOps / untagged jobs
dashboard = true # scoped k8s view + Grafana board
}
}
tf-apply provisions: a project-locked
runner in the fenced <slug>-ci namespace,
ResourceQuota/LimitRange (the costed bound — default 8Gi limits: runner
+ two concurrent kaniko builds), scoped RBAC, a GitLab environment,
user_access kubectl (Operate → Kubernetes, their namespace
only) and a per-tenant Grafana folder+board.
authentik — create group
tenant:<slug> as a child of vpn-users
(terraform: one authentik_group resource beside
tenant_msk in authentik-access.tf), then
add the tenant’s user to it. The nested chain
fastMCP ← vpn-users ← tenant:<slug> means that ONE
membership grants VPN auth, MCP access, and their MCP scoping claim.
Removing it — or disabling the user — kills all three.
That’s it. No CI variables, no secrets handed over, no clicks beyond the group tick.
| Capability | Where | Notes |
|---|---|---|
| CI runner | their project, tag <slug>-ci + untagged |
project-locked; jobs are pods in their fenced namespace |
| Container registry | registry.chardma.org.uk/<namespace>/<project> |
per-project, GitLab-auth’d |
| Package registry | GitLab Packages (npm/PyPI/Maven/generic) | on the main host |
| Static hosting | pages.chardma.org.uk/<namespace>/<project>/ |
GitLab Pages, namespace-in-path |
| k8s view | GitLab → Operate → Kubernetes | their namespace only (user_access) |
| Metrics | per-tenant Grafana board | their namespace’s usage |
| VPN | L2TP/IPsec to baldwin.chardma.org.uk |
authentik credentials — see below |
| MCP (estate tools, scoped) | https://fastmcp.chardma.org.uk/mcp over the VPN |
OAuth via authentik; the tenant:<slug> claim
scopes them to their own namespace/project + a read-only toolset |
Built-in L2TP/IPsec client:
| Field | Value |
|---|---|
| VPN type | L2TP/IPsec with pre-shared key |
| Server | baldwin.chardma.org.uk |
| Pre-shared key | issued by the platform (the tunnel secret) |
| Username / password | their authentik login |
| Authentication | PAP inside the tunnel (the gateway requires it; IPsec encrypts the wire) |
Caveat: Android 12+ removed native L2TP — Windows/macOS/iOS are fine; Android needs a strongSwan-class app (or waits for the WireGuard follow-up).
Privileged builds (docker/DinD) are disabled on tenant runners — that isolation is the product. Build with kaniko (rootless), pushing to your own project registry:
# .gitlab-ci.yml — override the Auto DevOps build job, or use standalone
build:
stage: build
image:
name: docker.chardma.org.uk/kaniko-executor:v1.23.2-debug
entrypoint: [""]
script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf '%s:%s' ${CI_REGISTRY_USER} ${CI_REGISTRY_PASSWORD} | base64 -w0)\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
--destination "${CI_REGISTRY_IMAGE}:latest"Using Auto DevOps? Disable its docker-based build and keep the rest:
include:
- template: Auto-DevOps.gitlab-ci.yml
variables:
CODE_QUALITY_DISABLED: "true"
build: # shadow Auto DevOps' DinD build with the kaniko job above
...Notes: the runners are arm64 (build multi-arch or target arm64); jobs get ~2Gi memory (the LimitRange cap) — two builds can run concurrently within the tenant quota.
The tenancy’s bound = the ResourceQuota (tunable per tenant in tfvars). Per-tenant costing (PoE wattage + traffic + quota → the Grafana “bill”) is the roadmap item; the quota is the enforcement point today.
<slug>-ci only.user_access) and the MCP toolset are
namespace/project-scoped.