| 1 | {{define "fleet_byo"}}{{template "top" .}} |
| 2 | {{if .Flash}}<p class="banner">{{.Flash}}</p>{{end}} |
| 3 | {{if .FlashErr}}<p class="banner error">{{.FlashErr}}</p>{{end}} |
| 4 | <div class="pagehead"> |
| 5 | <p class="eyebrow">Fleet · {{.Account.Name}}</p> |
| 6 | <h1>Bring your own fleet</h1> |
| 7 | <p class="lede">Run your apps on hardware you own. Your boxes dial out to the platform over an |
| 8 | outbound tunnel; nothing ever connects in.</p> |
| 9 | </div> |
| 10 | |
| 11 | <h2>What you need</h2> |
| 12 | <ul> |
| 13 | <li>A Linux box, arm64 or amd64, that can reach the internet outbound.</li> |
| 14 | <li>An object-storage bucket you own for the fleet's data.</li> |
| 15 | <li>Nothing listens inbound, ever — no port to open, no firewall rule to punch a hole in.</li> |
| 16 | </ul> |
| 17 | |
| 18 | <h2>Create the fleet</h2> |
| 19 | <p>From a terminal signed in to this console:</p> |
| 20 | <pre><code>carlos fleets create -bucket <your-bucket> <name> |
| 21 | carlos fleets add-box <name> <label></code></pre> |
| 22 | <p class="small">(shown once — the console does not store or re-show the plaintext)</p> |
| 23 | <p class="small">ROTATION IS NOT REVOCATION: this refuses the box's NEXT dial-in with the OLD |
| 24 | token, but does NOT evict a channel it already holds — a COMPROMISED box needs Detach, not |
| 25 | another Rotate.</p> |
| 26 | |
| 27 | <h2>Configure the box</h2> |
| 28 | <p>On the box, in a root-owned env file the agent's unit loads (see Hardening below):</p> |
| 29 | <pre><code>AWS_SHARED_CREDENTIALS_FILE=/etc/carlos/credentials |
| 30 | CARLOS_HIBERNATE_BUCKET=<your-bucket> |
| 31 | CARLOS_FLEET=<name> |
| 32 | CARLOS_FLEET_BOX=<label> |
| 33 | CARLOS_REGION=fleet/<name> |
| 34 | CARLOS_RELEASE_PUBKEY=<this deployment's release public key> |
| 35 | CARLOS_TUNNEL_URL=<this deployment's tunnel address> |
| 36 | CARLOS_TUNNEL_TOKEN=<from carlos fleets add-box <name> <label> — shown once> |
| 37 | CARLOS_TUNNEL_NAME=<name>/<label></code></pre> |
| 38 | <p class="small">Release pointers are Ed25519-signed; without <code>CARLOS_RELEASE_PUBKEY</code> the |
| 39 | box has no key to verify against and refuses to adopt anything.</p> |
| 40 | <p class="small"><code>CARLOS_FLEET_BOX</code> is optional and defaults from |
| 41 | <code>CARLOS_TUNNEL_NAME</code>'s label half. <code>AWS_SHARED_CREDENTIALS_FILE</code> points at |
| 42 | an access key for the bucket above — <strong>your own bucket only</strong> — written root-owned |
| 43 | mode 600 alongside the env file.</p> |
| 44 | <p class="small">No <code>CARLOS_DEPLOYMENT_BUCKET</code>, and no AWS credentials for the |
| 45 | platform's own bucket, belong in this file at all: the box reads its app releases and writes its |
| 46 | heartbeats through the console's edge, over HTTPS, using the same tunnel token it already dials |
| 47 | in with — not a bucket-level AWS grant.</p> |
| 48 | <p class="small">Optional override: <code>CARLOS_STATUS_PREFIX=fleets/<name>/</code> — the box |
| 49 | defaults to this on its own; set it explicitly only if you're pointing status/heartbeats |
| 50 | somewhere non-standard.</p> |
| 51 | |
| 52 | <h3>Legacy direct-S3 (grandfathered)</h3> |
| 53 | <p class="small">A box still running a pre-fleet-API agent build needs |
| 54 | <code>CARLOS_DEPLOYMENT_BUCKET</code> set to the platform's own deployment bucket, plus an AWS |
| 55 | credential granting it RW there, in addition to the hibernate-bucket vars above. New boxes should |
| 56 | not be configured this way — it is kept working for boxes that haven't upgraded yet, not as a |
| 57 | choice.</p> |
| 58 | |
| 59 | <h2>Hardening</h2> |
| 60 | <ul> |
| 61 | <li>Run the agent as a dedicated non-root user, not root.</li> |
| 62 | <li>Make the env file root-owned, mode 600 — it holds the tunnel token.</li> |
| 63 | <li>Default-deny inbound on the box's firewall; it only ever dials out.</li> |
| 64 | <li>Turn on unattended security upgrades.</li> |
| 65 | <li>Keep the clock sane — chrony or systemd-timesyncd; leases compare timestamps.</li> |
| 66 | <li>Run the agent under a systemd unit with <code>ProtectSystem=strict</code> and |
| 67 | <code>NoNewPrivileges=yes</code> — see <code>internal/host/units/</code> in the platform repo |
| 68 | for the reference shapes.</li> |
| 69 | <li>Rotate the token if it may have leaked. If the box itself may be compromised, |
| 70 | <strong>Detach</strong> it instead — rotating only refuses its next dial-in, it does not evict |
| 71 | a channel the box already holds.</li> |
| 72 | </ul> |
| 73 | |
| 74 | <h2>Then</h2> |
| 75 | <p>The box appears on your fleet page within minutes — it self-reports on a heartbeat cadence, not |
| 76 | instantly. From there, place an app on it with <code>carlos apps place</code> or from the app's |
| 77 | Instances tab.</p> |
| 78 | {{template "bottom" .}}{{end}} |
| 79 | |