Runtime Boundaries
AtomixOS separates immutable platform code from operator-provisioned runtime behavior.
Immutable Platform
The image owns boot, kernel, initrd, RAUC, firewall defaults, SSH policy, local provisioning, LAN gateway services, OpenVPN recovery plumbing, and update confirmation logic. These live in the active squashfs slot and are replaced only by RAUC updates.
Persistent Operator State
/data/config/ owns runtime configuration imported during provisioning. RAUC slot writes do not modify /data.
Before initial provisioning, the bootstrap API is reachable on WAN and LAN and exposes POST /api/config for complete
config.toml files or supported config bundles. First-boot Boot UI submissions use a CSRF bootstrap token, not operator
authentication; first-boot programmatic /api/config submissions do not require that UI token. After provisioning, the
bootstrap API narrows to the LAN gateway endpoint. It uses the same validation, candidate promotion, activation, and
rollback path as the web console. Programmatic
clients receive 202 Accepted with job_id, initial state, job_url, and a Location: /api/jobs/{id} header, then
poll the job resource for final success, failure, rollback status, and service deployment events.
The API routes retain operation IDs and domain tags in code, and the production bootstrap service exposes live OpenAPI schema routes for online clients. Response bodies are typed in the provisioning package schemas while preserving the current JSON shapes.
The accepted config.toml schema is:
version = 1
[users.admin]
isAdmin = true
ssh_key = "ssh-ed25519 ..."
[network.firewall.inbound.wan]
tcp = [443]
udp = [1194]
[network.dnsmasq]
gateway_cidr = "172.20.30.1/24"
dhcp_start = "172.20.30.10"
dhcp_end = "172.20.30.254"
domain = "local"
gateway_aliases = ["atomixos"]
hostname_pattern = "atomixos-{mac}"
[network.ntp]
servers = ["time.cloudflare.com"]
[activation]
required = ["myapp"]
[containers.container.myapp]
privileged = false
[containers.container.myapp.Container]
Image = "ghcr.io/example/myapp:latest"
PublishPort = ["10080:8080"]
WAN ports stay deny-by-default unless listed. LAN stays open by default; if [network.firewall.inbound.lan] is
present with any ports, LAN switches to an explicit allowlist for only those ports. [network.dnsmasq] is optional;
omitted fields use the fallback LAN gateway contract. [network.ntp] is optional and defaults to Cloudflare NTP.
The machine-readable schema is committed at
schemas/config.schema.json and the import path validates against it before semantic checks.
Firewall JSON
/data/config/firewall-inbound.json is a JSON object with optional wan and lan objects. Each scope may contain
optional tcp and udp arrays of integer ports in 1..65535.
{
"wan": {
"tcp": [443],
"udp": [1194]
},
"lan": {
"tcp": [443]
}
}
Provisioned rules are added to WAN eth0 or LAN eth1 only when the matching scope is present. WAN remains
deny-by-default for new inbound traffic. LAN is open by default, but an explicit lan scope replaces that default-open
rule with the configured allowlist. Forwarding remains dropped.
LAN JSON
/data/config/lan-settings.json is generated from config.toml and includes the validated runtime fields consumed by
lan-gateway-apply.py.
{
"gateway_cidr": "172.20.30.1/24",
"gateway_ip": "172.20.30.1",
"subnet_cidr": "172.20.30.0/24",
"netmask": "255.255.255.0",
"dhcp_start": "172.20.30.10",
"dhcp_end": "172.20.30.254",
"domain": "local",
"hostname_pattern": "atomixos-{mac}",
"gateway_aliases": ["atomixos"]
}
The DHCP range must stay inside the /24 gateway subnet, must be ordered, and must not include the gateway IP.
Quadlet Safety Boundary
Provisioned containers are rendered into canonical Quadlet files under /data/config/quadlet/ before being synced into
Podman systemd search paths.
Rootful containers require privileged = true and are forced onto Network=host. Rootless containers use the appsvc
user, are forced onto Network=pasta, and non-loopback PublishPort binds are rewritten to 127.0.0.1.
Bundle imports may include files/; Quadlet values may reference ${CONFIG_DIR} and ${FILES_DIR} to bind files from
/data/config/ without embedding host-specific absolute paths in the seed.