# Requirements

### Server Stack

#### Operating system

- <span class="font-semibold" data-streamdown="strong">Supported:</span> Debian/Ubuntu and <span class="font-semibold" data-streamdown="strong">RHEL family</span> (AlmaLinux, Rocky, CentOS).
- <span class="font-semibold" data-streamdown="strong">Init:</span> <span class="font-semibold" data-streamdown="strong">systemd</span> (required — agent is designed as a systemd unit).
- <span class="font-semibold" data-streamdown="strong">Architecture:</span> Linux x86\_64 (typical VPS; agent uses standard distro package managers).

#### Runtime dependencies  


<div class="ui-scroll-area" data-direction="horizontal" data-scroll-padding="4" data-visibility="hover" id="bkmrk-component-required-n"><div class="ui-scroll-area__viewport"><div class="ui-scroll-area__content"><table><thead class="bg-muted/80" data-streamdown="table-header"><tr class="border-border border-b" data-streamdown="table-row"><th class="whitespace-nowrap px-4 py-2 text-left font-semibold text-sm" data-streamdown="table-header-cell">Component</th><th class="whitespace-nowrap px-4 py-2 text-left font-semibold text-sm" data-streamdown="table-header-cell">Required</th><th class="whitespace-nowrap px-4 py-2 text-left font-semibold text-sm" data-streamdown="table-header-cell">Notes</th></tr></thead><tbody class="divide-y divide-border bg-muted/40" data-streamdown="table-body"><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">Python 3.9+</span></div></td><td><div class="md-table-cell-content">Yes</div></td><td><div class="md-table-cell-content">Stdlib only; on EL8 minimal images may need `python39`</div></td></tr><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">systemd</span></div></td><td><div class="md-table-cell-content">Yes</div></td><td><div class="md-table-cell-content">Service: `<span class="md-inline-path-filename">balctl-heartbeat.service</span>`</div></td></tr><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">wget</span> or <span class="font-semibold" data-streamdown="strong">curl</span></div></td><td><div class="md-table-cell-content">Install-time</div></td><td><div class="md-table-cell-content">Download `<span class="md-inline-path-filename">agent.zip</span>`</div></td></tr><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">unzip</span></div></td><td><div class="md-table-cell-content">Install-time</div></td><td><div class="md-table-cell-content">Extract bundle</div></td></tr><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">sudo / root</span></div></td><td><div class="md-table-cell-content">For full feature set</div></td><td><div class="md-table-cell-content">Heartbeat itself can run unprivileged; most panel jobs need root</div></td></tr></tbody></table>

</div></div></div>#### Optional packages (installed by agent jobs when needed)

<div class="ui-scroll-area" data-direction="horizontal" data-scroll-padding="4" data-visibility="hover" id="bkmrk-package-when-haproxy"><div class="ui-scroll-area__viewport"><div class="ui-scroll-area__content"><table><thead class="bg-muted/80" data-streamdown="table-header"><tr class="border-border border-b" data-streamdown="table-row"><th class="whitespace-nowrap px-4 py-2 text-left font-semibold text-sm" data-streamdown="table-header-cell">Package</th><th class="whitespace-nowrap px-4 py-2 text-left font-semibold text-sm" data-streamdown="table-header-cell">When</th></tr></thead><tbody class="divide-y divide-border bg-muted/40" data-streamdown="table-body"><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">haproxy</span></div></td><td><div class="md-table-cell-content">Install HAProxy job or `BALCTL_PROVISION_HAPROXY=1`</div></td></tr><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">socat</span></div></td><td><div class="md-table-cell-content">Admin socket drain/ready, runtime HAProxy commands</div></td></tr><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">firewalld</span></div></td><td><div class="md-table-cell-content">RHEL-family firewall jobs (auto-installed on first “Refresh rules” if missing, agent v78+)</div></td></tr><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">ufw</span></div></td><td><div class="md-table-cell-content">Debian firewall backup jobs</div></td></tr></tbody></table>

</div></div></div>### Network requirements

#### Outbound HTTPS (required)

The VM must reach:

<div class="ui-scroll-area" data-direction="horizontal" data-scroll-padding="4" data-visibility="hover" id="bkmrk-destination-purpose-"><div class="ui-scroll-area__viewport"><div class="ui-scroll-area__content"><div class="ui-scroll-area" data-direction="horizontal" data-scroll-padding="4" data-visibility="hover"><div class="ui-scroll-area__viewport"><div class="ui-scroll-area__content"><table><thead class="bg-muted/80" data-streamdown="table-header"><tr class="border-border border-b" data-streamdown="table-row"><th class="whitespace-nowrap px-4 py-2 text-left font-semibold text-sm" data-streamdown="table-header-cell">Destination</th><th class="whitespace-nowrap px-4 py-2 text-left font-semibold text-sm" data-streamdown="table-header-cell">Purpose</th></tr></thead><tbody class="divide-y divide-border bg-muted/40" data-streamdown="table-body"><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">`https://serversctl.com`</span> (or your `BALCTL_API_BASE`)</div></td><td><div class="md-table-cell-content">Heartbeat, job claim/complete, backup upload/download</div></td></tr><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">`https://download.serversctl.com/agent.zip`</span></div></td><td><div class="md-table-cell-content">Self-update (default)</div></td></tr><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">`https://api.ipify.org`</span> (optional)</div></td><td><div class="md-table-cell-content">Public IPv4 discovery when `BALCTL_PROBE_PUBLIC_IP=1`</div></td></tr></tbody></table>

</div></div></div></div></div></div>All agent API traffic must use <span class="font-semibold" data-streamdown="strong">HTTPS</span> — the agent refuses plaintext `BALCTL_API_BASE` / `BALCTL_UPDATE_URL` (v28+).

#### Inbound (not required)

Panel-driven operations use the <span class="font-semibold" data-streamdown="strong">outbound job queue</span>. No inbound SSH or agent port is required if the agent runs as <span class="font-semibold" data-streamdown="strong">root</span> for privileged jobs.

#### IP allowlisting (enrollment)

When you create a pool member, you must supply <span class="font-semibold" data-streamdown="strong">at least one allowed source IPv4</span>. This is the VM’s <span class="font-semibold" data-streamdown="strong">outbound/egress IP</span> as seen when it calls the control plane — <span class="font-semibold" data-streamdown="strong">not necessarily</span> its SSH IP or the IP traffic should hit.

The control plane validates <span class="font-semibold" data-streamdown="strong">`CF-Connecting-IP`</span> against the enrolled allowlist on every agent request. Mismatch → <span class="font-semibold" data-streamdown="strong">403</span>.

#### Enrollment requirements

Before the agent can heartbeat, create the member in the dashboard (<span class="font-semibold" data-streamdown="strong">Add pool member</span>):

<div class="ui-scroll-area" data-direction="horizontal" data-scroll-padding="4" data-visibility="hover" id="bkmrk-field-requirement-ho"><div class="ui-scroll-area__viewport"><div class="ui-scroll-area__content"><div class="ui-scroll-area" data-direction="horizontal" data-scroll-padding="4" data-visibility="hover"><div class="ui-scroll-area__viewport"><div class="ui-scroll-area__content"><table><thead class="bg-muted/80" data-streamdown="table-header"><tr class="border-border border-b" data-streamdown="table-row"><th class="whitespace-nowrap px-4 py-2 text-left font-semibold text-sm" data-streamdown="table-header-cell">Field</th><th class="whitespace-nowrap px-4 py-2 text-left font-semibold text-sm" data-streamdown="table-header-cell">Requirement</th></tr></thead><tbody class="divide-y divide-border bg-muted/40" data-streamdown="table-body"><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">Hostname</span></div></td><td><div class="md-table-cell-content">Must match the JSON `hostname` the agent sends (case-insensitive). Override with `BALCTL_HOSTNAME` if OS hostname differs.</div></td></tr><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">Allowed source IPs</span></div></td><td><div class="md-table-cell-content">One or more IPv4 addresses (comma-separated). Must include egress IP to control plane.</div></td></tr><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">Enrollment secret</span></div></td><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">48 hex characters</span>, no hyphens. Shown <span class="font-semibold" data-streamdown="strong">once</span> in the modal. <span class="font-semibold" data-streamdown="strong">Not</span> the member UUID on the card.</div></td></tr><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">Member template</span></div></td><td><div class="md-table-cell-content">e.g. <span class="font-semibold" data-streamdown="strong">HAProxy balancer</span> — determines which panel commands are available.</div></td></tr><tr class="border-border border-b" data-streamdown="table-row"><td><div class="md-table-cell-content"><span class="font-semibold" data-streamdown="strong">Linux family</span></div></td><td><div class="md-table-cell-content">Debian/Ubuntu vs RHEL — affects generated install one-liner.</div></td></tr></tbody></table>

</div></div></div></div></div></div>#### Authentication model

<div class="ui-scroll-area" data-direction="horizontal" data-scroll-padding="4" data-visibility="hover" id="bkmrk-header%3A%C2%A0authorizatio"><div class="ui-scroll-area__viewport"><div class="ui-scroll-area__content">- Header: `Authorization: Bearer <48-char-enrollment-secret>`
- Secret stored server-side as SHA-256 hash only.
- <span class="font-semibold" data-streamdown="strong">401</span> = wrong/unknown secret.
- <span class="font-semibold" data-streamdown="strong">403</span> = IP not allowlisted, or hostname mismatch, or missing `CF-Connecting-IP`.

  
</div></div></div>