Skip to content

API

strongholdd's HTTP API is how everything manages a box: the Cockpit plugin, the stronghold CLI and the Ansible collection (ansible/). Its contract is the OpenAPI document internal/api/openapi.yaml, served at /v1/openapi.yaml; a test fails if a route and the document disagree.

Where it is

  • On the box: the Unix socket /run/stronghold/api.sock, which only root can open. No token: whoever can open it is root. X-Stronghold-User names who a change is made for (the history's author).
  • Cockpit reaches it as the administrator (cockpit.http with superuser: "require"); without administrative access the UI asks for it.
  • sudo stronghold ... uses it.
  • Ansible modules running on the box (become: true) use it.
  • Over the network: HTTPS, when stronghold.yaml says where:
api:
  listen: [":8443"]          # or an address on a management network

It is a port on the box like any other, so the firewall must allow it (the built-in port object stronghold-api, tcp/8443) from the zones that may use it; nothing is open by default. Every request needs a token (Authorization: Bearer sht_…):

sudo stronghold token create ansible     # shown once; the box keeps a hash
sudo stronghold token create grafana viewer
stronghold --url https://gw1:8443 --token sht_… status

Tokens are managed only on the socket; a token's name is the author of its changes, and its role (below) what it may do: admin unless given another (stronghold token role NAME ROLE changes it, the token stays). The box makes a self-signed certificate (/var/lib/stronghold/api/tls.crt) unless it is given one there.

The standalone UI

The same pages as the Cockpit plugin, without Cockpit: strongholdd serves them on the HTTPS listener at / (https://gw:8443/), with ui/standalone/cockpit.js in place of Cockpit's script. That file provides the little of Cockpit's API the pages use (HTTP, location, user) on top of this API, and the shell: a login page and a bar with the user, the theme and "Log out". Cockpit stays the default on a box; the standalone UI is for where there is no Cockpit, or no system users.

Logging in gives a session (POST /v1/session), sent like a token, kept in the tab (sessionStorage), ended after an hour idle or 12 hours:

  • Password: PAM's stronghold service, for root and members of wheel (admins), and of api.operator-groups and api.viewer-groups (strongholdd built with -tags pam, as the image is).
  • Token: one made with stronghold token create, with its role.
  • Single sign-on: an OpenID Connect provider such as Keycloak:
# stronghold.yaml
api:
  listen: ["10.71.1.1:8443"]
  oidc:
    issuer: https://sso.example.net/realms/home
    client-id: stronghold
    name: Keycloak                    # "Log in with Keycloak"
    allowed-groups: [stronghold-admins]   # admins
    operator-groups: [netops]
    viewer-groups: [helpdesk]
    # username-claim: preferred_username, groups-claim: groups (defaults)

The client secret goes with the change (oidc-client-secret in the api section's body) and is kept as a secret on the box. strongholdd uses the authorization code flow with PKCE and checks the ID token itself (signature against the provider's keys, issuer, audience, expiry, nonce); only users in one of the groups get in, with the highest role of their groups. Logging out ends the provider's session too. In Keycloak: a confidential client with the standard flow and PKCE S256, valid redirect URI https://<box>:8443/v1/session/oidc/callback, post-logout redirect URI https://<box>:8443/, and a "Group Membership" mapper with the claim name groups.

The UI's address needs the firewall to allow stronghold-api from where people manage the box, as for the API.

Roles

Every login has a role, and every operation needs one (x-role in openapi.yaml); a higher role may do what a lower one may:

Role May
viewer read the config, the live state, the history and the notices; test a packet against the firewall
operator also run the troubleshooting tools, scan for Wi-Fi, drop DHCP leases and dynamic DNS records, move the HA pair (maintenance, handover, takeover), check for updates and push the config to git
admin everything else: change the config, confirm or revert a change, VPN keys and client configs, the pairing key, updates, backups and restores
  • The Unix socket is root's, so admin: the Cockpit plugin (as the administrator) and sudo stronghold.
  • A token has its role.
  • A password login: admin for root and wheel, else the highest role of the user's groups in api.operator-groups and api.viewer-groups (stronghold.yaml). Anyone else can't log in.
  • Single sign-on: the highest role of the user's groups at the provider (allowed-groups are admins, operator-groups, viewer-groups).

A request the role may not make gets 403 (forbidden). A session keeps the role it got at login: a change to the groups applies at the next login. The standalone UI shows a viewer's or operator's role in its bar.

# stronghold.yaml
api:
  listen: [":8443"]
  operator-groups: [netops]
  viewer-groups: [helpdesk]

Changes

PUT /v1/config/{section} replaces one section (firewall, interfaces, dhcpdns, dhcp, dns, ha, loadbalancer, proxy, vpn, and stronghold.yaml's parts; the full list is openapi.yaml's) with a body shaped like its YAML file:

  • It is applied at once and rolled back unless POST /v1/pending/confirm comes within 60 seconds; one change waits at a time (409 otherwise).
  • A body that changes nothing is not applied: {"changed": false}.
  • ?dry_run=true validates the change and returns what it would alter (diff: each changed file before and after) without making it.
  • Problems with the config: 422 with every problem in problems.
  • Secrets come with the change and are never in the config: Wi-Fi passwords (wifi-passwords with interfaces), WireGuard preshared keys (preshared-keys with vpn), the web proxy's keytab ({proxy, keytab} with proxy; POST /v1/proxy/join gets one from the domain, proxy.md). Passing one that is already stored is no change.
  • POST /v1/functions/{name}/enable is a change too: a function turned here, elsewhere or off, with what that takes (functions.md, "Installing"); POST /v1/functions/{name}/install installs its packages first, in the background (package mode). GET /v1/functions/dns-auth/key (admin, not cached) is the DDNS key a server for the local zones elsewhere must know.

Live state and tools

The things the UI used to run commands for are API calls now: /v1/system/nics, /v1/system/addresses and /v1/system/metrics (raw counters; rates come from two samples), /v1/firewall/counters (each rule's hits, firewall.md). The troubleshooting tools are POST /v1/tools/{tool} with their parameters: strongholdd builds each tool's fixed command lines from checked values and streams the output as text. It never runs a command line it is given.

CLI

stronghold status | show | pending | history | confirm | revert
stronghold set firewall ./config --dry-run       # shows the diff
stronghold set firewall ./config -m "Open 443"   # then: stronghold confirm
stronghold tool                                  # the tools
stronghold tool ping target=9.9.9.9 count=3
stronghold get /v1/wan/status                    # any GET, as JSON
stronghold token list | create NAME [ROLE] | role NAME ROLE | delete NAME
stronghold firewall test 10.71.1.10 internet tcp 443   # which rule, and why
stronghold vpn ipsec                             # the site-to-site tunnels, certificates' expiry
stronghold vpn ipsec psk office --generate       # a tunnel's pre-shared key
stronghold vpn ipsec cert hq --cert hq.crt --key hq.key --ca ca.crt   # or: --ca ca.crt alone
stronghold vpn openvpn                           # the OpenVPN servers and their clients
stronghold vpn openvpn client road > road.ovpn   # a client's config (add its certificate)
stronghold counters lan gateway                  # the rules' hits
stronghold dhcp leases | release 10.71.1.150
stronghold proxy | join -u Administrator | keytab proxy.keytab   # the proxies (proxy.md)
stronghold tool site host=cloud.example.com     # a site, through the reverse proxy
stronghold dns records home.arpa | delete home.arpa pc A 10.71.1.150
stronghold functions enable dhcp elsewhere --servers 10.0.0.67 --networks lan
stronghold functions key --bind                  # the DDNS key
stronghold setup finish MyWiFi                   # first boot's setup network

Every action of the UI has its CLI command (any GET through get), but the WireGuard client flow, a peer's key pair and its config with a QR code (POST /v1/vpn/keys, /v1/vpn/client-config), which the UI walks through (vpn.md).

--url and --token (or STRONGHOLD_URL, STRONGHOLD_TOKEN) point it at another box; --insecure accepts a self-signed certificate.