Bring your own cloud
CLANKERNET runs in your accounts from day one. What runs there is the factory — a host, Traefik-fronted developer containers and the GitFWD coordination gateway, for whatever repository you are building — never a product of ours: clankerengineer is a separate, hosted sister product and is not installed in a customer’s cloud (plan D25). There is no hosted control plane, no vendor-held cloud credential, and nothing to revoke on our side because we were never granted anything. This page is the bill of materials you need before writing a host spec, an honest account of what lives in your state, the shared-responsibility split, and the v1 limits with the task that lifts each.
This page is the canonical home of the trust model (plan D21): BYOC is this product’s shape, so the boundary contract, the state-custody table and the shared-responsibility split live here and nowhere else — the Clanker product’s enterprise plan, where an earlier draft of the same model once lived, has been deleted and points at nothing. The compliance program that surrounded that draft (SOC 2 / ISO / GDPR sections) is not carried; it belongs to whichever product sells it.
Bill of materials
What the host owner must already have. A prospect who cannot hold a Cloudflare account is not a factory prospect; say so in the first call.
| You need | Because | Host spec key |
|---|---|---|
| A Cloudflare account and one zone in it | Mandatory: cloudflare-tunnel and cloudflare are the only v1 ingress and DNS providers. Free plan is enough; Universal SSL covers one label under the zone | ingress, dns.zones |
| A Cloudflare API token: Tunnel edit + Zone:DNS edit on that zone | Creates the tunnel; writes the tagged records | ingress.tokenFrom, dns.tokenFrom |
| Either a Hetzner project + API token | The factory creates a cpx32 (default) Ubuntu 24.04 server, a firewall and an SSH key | compute.provider: hetzner |
| or an Ubuntu 24.04 VM you already run, with root SSH | Converged over SSH; outbound UDP/7844 (the tunnel) is the only network requirement | compute.provider: existing-host |
Either a Tailscale tailnet + an OAuth client (scopes auth_keys write, devices write) and an ACL tag | Containers join the tailnet; developers reach them there. Pre-auth keys are minted on the runner per provision | access.provider: tailscale |
or nothing — ssh-direct | sshd on the host’s public address. You must write compute.firewall.ssh: true yourself | access.provider: ssh-direct |
| Either a Pulumi Cloud org | State under your org; secrets as pulumi config set --secret blobs committed in the stack file (the reference deployment, plan D24) or in an ESC environment; CI needs only PULUMI_ACCESS_TOKEN | state.provider: pulumi-cloud, secrets.provider: pulumi-config or pulumi-esc |
| or an S3-compatible bucket (R2 works) + access keys + a passphrase or KMS key | Self-managed state; three caller secrets (FACTORY_PASSPHRASE, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) | state.provider: s3, secrets.provider: pulumi-config or factory-secrets |
| A GitHub PAT | repo scope for the container’s clone/push (plan D11); read:org only if access.github.org gates provisioning | env.git.tokenFrom, GITHUB_ORG_READ_TOKEN |
| Your own GitHub OAuth app + a coordination-repo token | Only if you run a gateway (gateway: in the host spec). An OAuth app has ONE callback URL — ${publicOrigin}/auth/github/callback — so it cannot be shared (plan D18); the token reads + pushes refs/heads/gitfwd/** | gateway.oauthFrom, gateway.gitTokenFrom, gateway.publicOrigin |
A GHCR read:packages token | v1 only: while CLANKERNET’s images are private (see limits) | image.pull.tokenFrom |
| Either an Upstash Redis you already run (its REST url + token) | Only if your containers consume a Redis. The existing form: the host declares nothing, creates nothing, and never holds an Upstash management key — the reference host reuses a database another environment owns | resources.upstash[].existing |
| or an Upstash management API key | The create form: the factory creates and owns the database ({ protect: true }; only resources destroy deletes it). For adopters who want the factory to own it | resources.upstash[].credentialsFrom |
| An R2 API token pair for each bucket the host provides | Only if your containers consume a bucket. The bucket is created by the host program; the S3 token is minted once in the dashboard and named here | resources.r2[].credentialsFrom |
Everything above is referenced by name. The names resolve in your secrets provider; the values never enter a file in either repo.
What we hold
Nothing.
- No cloud credential of yours. The reusable workflow runs in your
repository’s Actions, with your
PULUMI_ACCESS_TOKEN(or your S3 keys), reading your stack’s config or ESC environment.clankerlabs/CLANKERNETis a workflow definition and a composite action; it executes under your permissions. - No inbound path. Nothing on our side calls your host, your tunnel or your containers. Health probes run on your runner and report to your job summary.
- No telemetry. The engine emits no usage record to us; there is no crossing whose schema could carry one.
- No secret in a stack output.
tests/no-secret-outputs.test.ts(lands with the host engine, plan T021) pins that the host program exports onlyserverIp,tunnelId,tailscaleHostname, andhostSpec(which is non-secret by construction).
Revocation is unilateral and total: delete the caller workflow, or stop pinning our SHA. Running hosts and containers are unaffected; we could not update them anyway. A vendor that cannot be locked out has an unfalsifiable claim; this one can be locked out by removing one file.
What we DO control: the code your runner executes. Pin the reusable workflow
and the composite action to a 40-character SHA (Dependabot’s
github-actions ecosystem bumps it), never @v1, and read the diff. That is
the one crossing, and it is reviewable in git.
What is in your state
Pulumi state necessarily holds host secrets, because the resources are made of them: the tunnel’s credential, the Tailscale pre-auth key delivered at converge, the hcloud SSH key resource (public half). The control is who holds the backend and the decryption key, not “no secrets in state”:
| Backend | Who can read state | Who holds the decryption key |
|---|---|---|
pulumi-cloud | members of your Pulumi org with stack access | Pulumi Inc. (service-managed) — you accept them as a key holder |
s3 + passphrase | whoever can read the bucket | whoever knows FACTORY_PASSPHRASE |
s3 + awskms://… | whoever can read the bucket | your KMS key policy |
Not in state, by design:
- The host root private key — it lives in the secrets provider
(
compute.sshKeyFrom.privateKey, plan D19) and the program consumes it as an input, never as atls.PrivateKeyresource.factory host upmints the pair on your runner the first time and stores it there; no one hands a private key to anyone. - The gateway’s secrets (OAuth app, coordination-repo token, bridge key). They travel runner → host over SSH into a 0600 root env file per converge and are never Pulumi inputs, never in cloud-init.
- Any
env.secretsvalue. Those flow runner → container env file (0600) per provision and are never Pulumi inputs. - Any Tailscale OAuth client. The runner exchanges it for a tagged, one-use pre-auth key; only the key reaches the host, over SSH, never in cloud-init (cloud-init metadata is readable by any process on the box).
Per-container DNS records are not Pulumi resources at all — they are
idempotent API writes tagged managed-by=factory/<host>, so N provisions
never contend for one stack. Only host records are resources, and the
gateway record is { protect: true }.
Boundary contract
Every crossing between the CLANKERNET repository (ours) and your accounts (yours), with direction and payload:
| # | Crossing | Direction | Initiated by | Payload |
|---|---|---|---|---|
| 1 | Workflow + composite action checkout | ours → your runner | your workflow_dispatch | Source at the SHA you pinned. Contains no credential |
| 2 | Base image pull | GHCR → your host | your runner | ghcr.io/clankerlabs/factory-base@sha256:…, signed with provenance + SBOM (plan T051) |
| 3 | Cloud API calls (Hetzner, Cloudflare, Tailscale, Pulumi backend) | your runner → your accounts | your runner | Resource declarations under your tokens |
| 4 | SSH converge | your runner → your host | your runner | The tunnel token, the pre-auth key, compose files, the env file(s) — the gateway’s included |
| 5 | Job summary | your runner → your repo | your runner | Hostnames, onboarding card. Never a secret value |
There is no crossing from your accounts to ours. Customer data that never crosses in either direction: repository contents, env values, database rows, Tailscale device lists, DNS records, logs.
Shared responsibility
| Control | Us | You | Notes |
|---|---|---|---|
| Engine code, reusable workflow, composite action | ● | Pinned by SHA in your caller; reviewable | |
factory-base image and its provenance/SBOM | ● | Plan T051; you pin the tag | |
| Vulnerability disclosure → patched release | ● | We publish; you bump the pin | |
| Cloud account hardening, IAM, token scopes | ● | We name the minimum scopes; you issue the tokens | |
| Pulumi state backend and its key custody | ● | Table above | |
| Secrets provider contents and rotation | ● | stack config / ESC / FACTORY_SECRETS | |
| Host OS patching | ● | The converge script is idempotent; running it is yours to schedule | |
Who may provision (access.github, repo permissions) | ● | Authorisation = your repo’s workflow_dispatch permission, optionally tightened | |
Developer SSH key source (access.ssh.keySource) | ● | github-keys default; sshid is a third-party authority you opt into | |
| Backups of container volumes | ● | teardown preserves volumes; nothing copies them off-host | |
| Log retention, intrusion detection, incident response | ● | We cannot see inside your host. Support is consent-per-incident | |
| Subprocessors (Cloudflare, Hetzner, Tailscale, Pulumi Inc., GitHub) | ● | All under your accounts and your terms | |
| GHCR access while packages are private | ● | ● | We grant; you hold the pull token |
The uncomfortable row is intrusion detection. An appliance the vendor cannot observe is an appliance whose breach the vendor learns about from the customer; the answer to “what do you do when I report one” is: ship a fix under the disclosure row, and nothing else, because nothing else is possible.
v1 limitations
Each names the task that lifts it. Until then it is a fact, not a roadmap.
| Limitation | Lifted by |
|---|---|
Intra-org only. clankerlabs/CLANKERNET is private; its reusable workflow, composite action and GHCR packages are reachable only from clankerlabs repositories | T103 (go public) |
GHCR packages are private, so every host needs image.pull.tokenFrom | T103 |
s3 backends use static AWS keys; no OIDC (id-token: write is not requested) | T102 |
Secrets providers are pulumi-config, pulumi-esc, factory-secrets only — no AWS Secrets Manager / Vault | T102 |
Ingress is cloudflare-tunnel or none — no direct Caddy/ACME ingress | T102 |
| The container’s git credential is a PAT, not a GitHub App installation token | T102 |
existing-host is implemented but has no end-to-end test against a customer VM | T102 |
| One GFP/1 actor per container AND one per OAuth login (two actors per human) | T101 |
| The operated tier (“we run the clankerlabs host for you”) is the only offering; self-serve waits on the public repo | T103, T121 |