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-Usernames who a change is made for (the history's author). - Cockpit reaches it as the administrator (
cockpit.httpwithsuperuser: "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.yamlsays 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
strongholdservice, for root and members ofwheel(admins), and ofapi.operator-groupsandapi.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 inapi.operator-groupsandapi.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-groupsare 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/confirmcomes within 60 seconds; one change waits at a time (409 otherwise). - A body that changes nothing is not applied:
{"changed": false}. ?dry_run=truevalidates 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-passwordswith interfaces), WireGuard preshared keys (preshared-keyswith vpn), the web proxy's keytab ({proxy, keytab}with proxy;POST /v1/proxy/joingets one from the domain,proxy.md). Passing one that is already stored is no change. POST /v1/functions/{name}/enableis a change too: a function turned here, elsewhere or off, with what that takes (functions.md, "Installing");POST /v1/functions/{name}/installinstalls 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.