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 needBecauseHost spec key
A Cloudflare account and one zone in itMandatory: cloudflare-tunnel and cloudflare are the only v1 ingress and DNS providers. Free plan is enough; Universal SSL covers one label under the zoneingress, dns.zones
A Cloudflare API token: Tunnel edit + Zone:DNS edit on that zoneCreates the tunnel; writes the tagged recordsingress.tokenFrom, dns.tokenFrom
Either a Hetzner project + API tokenThe factory creates a cpx32 (default) Ubuntu 24.04 server, a firewall and an SSH keycompute.provider: hetzner
or an Ubuntu 24.04 VM you already run, with root SSHConverged over SSH; outbound UDP/7844 (the tunnel) is the only network requirementcompute.provider: existing-host
Either a Tailscale tailnet + an OAuth client (scopes auth_keys write, devices write) and an ACL tagContainers join the tailnet; developers reach them there. Pre-auth keys are minted on the runner per provisionaccess.provider: tailscale
or nothing — ssh-directsshd on the host’s public address. You must write compute.firewall.ssh: true yourselfaccess.provider: ssh-direct
Either a Pulumi Cloud orgState 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_TOKENstate.provider: pulumi-cloud, secrets.provider: pulumi-config or pulumi-esc
or an S3-compatible bucket (R2 works) + access keys + a passphrase or KMS keySelf-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 PATrepo scope for the container’s clone/push (plan D11); read:org only if access.github.org gates provisioningenv.git.tokenFrom, GITHUB_ORG_READ_TOKEN
Your own GitHub OAuth app + a coordination-repo tokenOnly 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 tokenv1 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 ownsresources.upstash[].existing
or an Upstash management API keyThe create form: the factory creates and owns the database ({ protect: true }; only resources destroy deletes it). For adopters who want the factory to own itresources.upstash[].credentialsFrom
An R2 API token pair for each bucket the host providesOnly 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 hereresources.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/CLANKERNET is 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 only serverIp, tunnelId, tailscaleHostname, and hostSpec (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”:

BackendWho can read stateWho holds the decryption key
pulumi-cloudmembers of your Pulumi org with stack accessPulumi Inc. (service-managed) — you accept them as a key holder
s3 + passphrasewhoever can read the bucketwhoever knows FACTORY_PASSPHRASE
s3 + awskms://…whoever can read the bucketyour 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 a tls.PrivateKey resource. factory host up mints 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.secrets value. 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:

#CrossingDirectionInitiated byPayload
1Workflow + composite action checkoutours → your runneryour workflow_dispatchSource at the SHA you pinned. Contains no credential
2Base image pullGHCR → your hostyour runnerghcr.io/clankerlabs/factory-base@sha256:…, signed with provenance + SBOM (plan T051)
3Cloud API calls (Hetzner, Cloudflare, Tailscale, Pulumi backend)your runner → your accountsyour runnerResource declarations under your tokens
4SSH convergeyour runner → your hostyour runnerThe tunnel token, the pre-auth key, compose files, the env file(s) — the gateway’s included
5Job summaryyour runner → your repoyour runnerHostnames, 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

ControlUsYouNotes
Engine code, reusable workflow, composite actionPinned by SHA in your caller; reviewable
factory-base image and its provenance/SBOMPlan T051; you pin the tag
Vulnerability disclosure → patched releaseWe publish; you bump the pin
Cloud account hardening, IAM, token scopesWe name the minimum scopes; you issue the tokens
Pulumi state backend and its key custodyTable above
Secrets provider contents and rotationstack config / ESC / FACTORY_SECRETS
Host OS patchingThe 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 volumesteardown preserves volumes; nothing copies them off-host
Log retention, intrusion detection, incident responseWe 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 privateWe 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.

LimitationLifted by
Intra-org only. clankerlabs/CLANKERNET is private; its reusable workflow, composite action and GHCR packages are reachable only from clankerlabs repositoriesT103 (go public)
GHCR packages are private, so every host needs image.pull.tokenFromT103
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 / VaultT102
Ingress is cloudflare-tunnel or none — no direct Caddy/ACME ingressT102
The container’s git credential is a PAT, not a GitHub App installation tokenT102
existing-host is implemented but has no end-to-end test against a customer VMT102
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 repoT103, T121