Getting started
Declarative fleet
One fleet.yaml describes every appliance — groups, patches, per-node facts — and `sihactl gen config` renders one ApplianceConfig document per appliance, ready to apply with client-side prune.
One fleet.yaml is the source of truth for a whole fleet. sihactl gen config
reads it and writes one ApplianceConfig document per appliance under configs/.
config apply on a document interprets it into COSI resources on the node, with
automatic client-side pruning — what you remove from the file disappears from
the node.
Source vs generated
fleet.yaml # SOURCE — you write and commit this
patches/hardening.yaml # reusable patch, @-included where needed
patches/edge-acl.yaml # another patch
gen/dev/configs/ # GENERATED by `gen config` (gitignored, never committed)
fw-1.yaml # fw-1's ApplianceConfig document
fw-2.yaml # fw-2's ApplianceConfig document
sihaconfig # your day-2 client identity
You edit fleet.yaml (+ patches); gen config writes everything under
configs/. That directory is gitignored (it may contain sensitive keys). Your
secret (the fleet CA) stays outside, SOPS-encrypted (see
Getting started).
fleet.yaml
version: v1alpha1
fleetName: prod
patches: # 1. global — every appliance
- "@./patches/hardening.yaml"
groups:
- name: edge
patches: ["@./patches/edge-acl.yaml"]
appliances:
- name: fw-1
endpoint: "192.0.2.10:6443"
groups: [edge]
network:
interfaces:
- name: wan
pci: "0000:05:00.0"
addresses: ["203.0.113.2/24"]
rxMode: adaptive
- name: lan
pci: "0000:06:00.0"
addresses: ["10.0.0.1/24"]
nat:
interfaces:
- name: lan-inside
interface: lan
role: inside
- name: wan-outside
interface: wan
role: outside
- name: fw-2
endpoint: "192.0.2.11:6443"
groups: [edge]
network:
interfaces:
- name: wan
pci: "0000:05:00.0"
addresses: ["203.0.113.3/24"]
- name: lan
pci: "0000:06:00.0"
addresses: ["10.0.0.1/24"]
nat:
interfaces:
- name: lan-inside
interface: lan
role: inside
- name: wan-outside
interface: wan
role: outside
fleetNameis informational.- Global
patchesreach every appliance. groupsare named sets with their own patches. An appliance opts in by listing group names.fw-1andfw-2both get theedgepatch; a third appliance that omitsgroups: [edge]is untouched.- Structured sections (
network:,nat:, …) state what this appliance IS — its ports and addresses. These are its facts; they beat global/group patches (more-specific-wins). - Per-appliance
patches(not shown) are last-mile exceptions that beat everything.
Cascade order
global patches
↓
group patches (in the appliance's groups list order)
↓
structured base (the appliance's own sections — beats patches)
↓
appliance patches (last-mile — beats everything)
Each step is a schema-aware strategic merge: lists merge by their key (e.g.
by name for interfaces), scalars replace, $patch: delete removes a keyed
entry, @./file.yaml inlines a file. Unknown fields are hard errors.
The rule: structured fields state what an appliance IS (its facts: ports, addresses, peers). Patches state shared policy (global/group) or last-mile exceptions.
ApplianceConfig — the document
Each gen config run writes one ApplianceConfig document per appliance. It is a
human-oriented YAML document — a designed schema with concrete section names.
COSI types never appear in it.
Sections:
| Section | What it configures |
|---|---|
machine.dataplane | VPP performance tier, hugepages, plugins |
machine.management | Day-2 management surface scoping (which physical NICs stay management-plane) |
network.interfaces | All VPP-owned interfaces (PMD, bond, bridge, tap, loopback, VXLAN) + sub-interfaces (vlans, qinq) |
routing.vrfs | IP routing tables |
routing.static | Named static routes |
routing.bgp | BGP per-VRF configuration + peers |
firewall.acls | ACLs with ordered rules (by seq:) and bindings (bindTo:) folded in |
nat | Interface roles, address pools, static + identity mappings |
loadBalancer | lb plugin (VIPs, NAT L4 services, interface modes) |
vrrp.virtualRouters | VRRP virtual routers |
dns.forwarders | DNS forwarding / caching |
dhcp.servers / .relays | Embedded DHCP server or relay |
Every operator-writable COSI type is reachable from the schema — enforced by the registry-driven coverage test.
Interface kinds are identified by which discriminator key is present:
| Key | Kind | |
|---|---|---|
pci: "0000:05:00.0" | pmd | DPDK PMD NIC bound by PCI address |
hostDevice: enp5s0 | host-interface | AF_PACKET debug attach |
bond: {members, mode} | bond | Link aggregation |
bridge: {members, bvi} | bridge | L2 bridge-domain |
tap: {hostDevice, ...} | tap | VPP host tap |
loopback: {} | loopback | Software loopback |
vxlan: {vni, local, remote, l3Peer} | vxlan | L3VNI tunnel |
VLANs and QinQ sub-interfaces nest INSIDE their parent entry:
- name: uplink
bond:
members: [upA, upB]
mode: xor
addresses: ["10.99.0.2/24"]
vlans:
- id: 100
addresses: ["10.100.0.2/24"]
qinq:
- outer: 200
inner: 100
addresses: ["10.200.0.2/24"]
This derives three Interfaces resources: uplink (bond), uplink.100 (VLAN
100), uplink.200.100 (QinQ).
ACL rules use seq: as both the merge key and the sort key; bindTo: is
folded in:
firewall:
acls:
- name: edge-in
family: IPv4
defaultAction: DROP
rules:
- seq: 10
action: ACCEPT
protocol: 1
description: "ICMP diagnostics"
- seq: 20
action: ACCEPT_STATEFUL
protocol: 6
destinationPortFirst: 443
bindTo:
- interface: wan
direction: INPUT
Patches — layered overrides
A patch is a fragment of an ApplianceConfig document (no version: field). It
merges by the same keys as the document. Examples:
Add a binding to an ACL without touching its rules (shows the recursive keyed-list merge):
firewall:
acls:
- name: edge-in
bindTo:
- { interface: dmz, direction: INPUT }
This adds the dmz binding; the wan binding and all rules are kept.
Remove one rule by seq:
firewall:
acls:
- name: edge-in
rules:
- seq: 20
$patch: delete
Remove an appliance from a group’s ACL (per-appliance patch):
firewall:
acls:
- name: edge-in
$patch: delete
Generate, validate, apply
# render fleet.yaml → configs/*.yaml + sihaconfig
sihactl gen config -f fleet.yaml --secret siha-secret.yaml
# inspect what would land on fw-1 (derived COSI types+ids, no apply)
sihactl gen config -f fleet.yaml --secret siha-secret.yaml --show-derived fw-1
# validate without applying (runs interpretation + adapter validation)
sihactl config validate -f configs/fw-1.yaml
# adopt the node (idempotent — safe to re-run)
sihactl adopt --secret siha-secret.yaml --addr 192.0.2.10:6443
# then apply (or re-apply) day-2 config
sihactl config apply --sihaconfig configs/sihaconfig --addr 192.0.2.10:6443 \
-f configs/fw-1.yaml
# one-shot ZTP alternative: adopt + apply in one command
sihactl config apply --insecure --secret siha-secret.yaml \
--sihaconfig configs/sihaconfig --addr 192.0.2.10:6443 \
-f configs/fw-1.yaml
Key points:
gen confignever touches a node — read and render only; safe to re-run (idempotent).config applyis declarative: what is in the document is the desired state. Resources of covered types that no longer appear in the document are deleted from the node (client-side prune).- Wipe guard:
--allow-wipeis required if the document derives zero resources (empty document against a configured node).--dry-runpreviews creates/updates/deletes without touching the node.
See CLI · sihactl for the full command surface.