VyOSLIV · API and AutomationAutomation
VyOS HTTP API — the /configure endpoint, key authentication, and one commit per request
What you'll learn
- Configure the VyOS HTTP API service with a named API key and a restricted listener
- Use the /configure endpoint to change the router, and understand that each request is its own commit
- Distinguish /retrieve (configuration) from /show (operational output) and persist changes with /config-file
- Recognise the production failure modes where the HTTP API is misconfigured or unsecured
Prerequisites
Verified against VyOS 1.5.x LTS (circinus) · VyOS 1.4.x (sagitta) — legacy · FRRouting 10.x (VyOS 1.5) · Linux kernel 6.6 LTS (VyOS 1.5 base) · strongSwan 5.9.x (IPsec) · WireGuard 1.0.x (kernel module + userspace tooling)
The VyOS HTTP API exposes the router over HTTPS. Instead
of SSH-ing in and running configure / set / commit,
an automation tool posts the operation as JSON and the
router applies it. The HTTP API is the foundation under
the Ansible collection, the configuration-as-code
pipeline, and every other tool that drives VyOS without
pretending to be a human at a terminal.
On VyOS 1.5 LTS the API is part of the service https
tree: service https owns the listener, the certificate
and the client allow-list, and service https api owns
the API itself and its keys. Authentication is a named
key sent with each request.
This lesson covers the service configuration, how a
request authenticates, the endpoints that matter
(/configure, /retrieve, /show, /config-file), and
the production failure modes of running a router this
way. The single most important of those failure modes is
not a security one: every successful /configure
request is its own commit, and that changes how a script
has to be written.
What the HTTP API exposes
The HTTP API is a second front door onto the same machinery the CLI drives — the same configuration tree, the same commit engine, the same operational commands:
flowchart LR
SSH["Operator<br/>SSH session"] --> C["configure"]
C --> SC["set / delete / edit"]
SC --> CC["commit engine"]
HTTPS["Automation<br/>HTTPS request"] --> A["HTTP API"]
A --> AC["/configure<br/>set / delete / comment"]
A --> AF["/config-file<br/>save / load"]
A --> AR["/retrieve<br/>read the config tree"]
A --> AS["/show<br/>operational output"]
AC --> CC
AF --> CC
AR --> CT["the configuration tree"]
AS --> OP["show ip route<br/>show ip bgp summary"]
Read the two halves carefully, because the split on the right is where most first-time API scripts go wrong:
/configuretakes configuration-mode operations —set,delete,comment— and applies them./config-filetakessaveandload: writing the running configuration to disk, or loading a configuration file./retrievereads the configuration tree (showConfig,returnValue,returnValues,exists). It is not an operational-state endpoint./showruns an operationalshowcommand and returns its output as text.
The router exposes further endpoints for images, generation, reset and reboot. The set differs slightly between releases, so treat your release’s documentation as authoritative rather than this list. VyOS also ships a GraphQL API under the same service; this lesson is about the REST endpoints.
Configuring the HTTP API service
configure
set service https api keys id automation key 'REPLACE-WITH-A-GENERATED-KEY'
set service https listen-address '10.99.0.1'
set service https allow-client address '10.99.0.0/24'
commit
save
Four things are worth separating out:
- The listener lives under
service https, not underservice https api.listen-addressandportare properties of the web server; the API is one thing that web server serves. Binding the listener to the management address is the single most effective control on this service. allow-client addressis a second, independent filter applied by the service itself, so a router whose management address is reachable from more places than you would like still has a purpose-built control that does not depend on the firewall being right.- Keys are named.
keys id automationis the key’s identity;key '...'is the secret. One key per automation tool is the production idiom, because it makes a single tool’s key rotatable in isolation and it makes the access log attributable. - The node set differs slightly between builds. Some
releases expose additional nodes under
service https api(for example an explicit REST toggle,debug, andstrict). Typeset service https api ?on the box in front of you before assuming a node exists or does not.
The default certificate is self-signed, which is why the
curl examples below pass -k. For anything beyond a
lab, install a real certificate into the pki tree and
point service https certificates at it, then drop the
-k — an automation client that has been taught to skip
certificate validation has been taught to accept a
man-in-the-middle.
How a request authenticates
The API does not use HTTP Basic authentication. The key
travels in the request, as a key field alongside the
data field carrying the operation:
KEY='REPLACE-WITH-A-GENERATED-KEY'
ROUTER='10.99.0.1'
curl -k -X POST "https://$ROUTER/retrieve" \
--form key="$KEY" \
--form data='{"op": "showConfig", "path": ["service", "https"]}'
A request with no key or a key the router does not know is rejected; a request with a valid key is executed with full privilege. There are no per-key permissions — a key that can read can also configure. That is the reason the listener restriction and the client allow-list matter as much as they do: the key is the whole authorisation model.
Responses are JSON in a fixed envelope, which is what makes the API scriptable at all:
{
"success": true,
"data": null,
"error": null
}
A script checks success and reports error. A script
that checks only the HTTP status code will one day report
a green deploy for a change the commit rejected.
The /configure endpoint
/configure takes a single operation, or a JSON array of
them:
KEY='REPLACE-WITH-A-GENERATED-KEY'
ROUTER='10.99.0.1'
curl -k -X POST "https://$ROUTER/configure" \
--form key="$KEY" \
--form data='{"op": "set", "path": ["protocols", "static", "route", "10.0.0.0/24", "next-hop", "192.0.2.1"]}'
Each element of path is one token of the CLI command, in
order — the JSON array above is exactly set protocols static route 10.0.0.0/24 next-hop 192.0.2.1 with the
spaces turned into commas. op is set, delete or
comment.
KEY='REPLACE-WITH-A-GENERATED-KEY'
ROUTER='10.99.0.1'
curl -k -X POST "https://$ROUTER/configure" \
--form key="$KEY" \
--form data='[
{"op": "set", "path": ["protocols", "static", "route", "10.0.0.0/24", "next-hop", "192.0.2.1"]},
{"op": "set", "path": ["protocols", "static", "route", "10.0.1.0/24", "next-hop", "192.0.2.1"]}
]'
KEY='REPLACE-WITH-A-GENERATED-KEY'
ROUTER='10.99.0.1'
curl -k -X POST "https://$ROUTER/config-file" \
--form key="$KEY" \
--form data='{"op": "save"}'
The same endpoint’s load operation replaces the
configuration from a file, which is the API-side
equivalent of load in configure mode — and carries the
same blast radius, because what it loads becomes the
whole configuration.
The /retrieve endpoint — reading the configuration
/retrieve reads the configuration tree. Its operations
are showConfig (return a subtree), returnValue and
returnValues (return the value or values at a leaf), and
exists (a boolean test):
KEY='REPLACE-WITH-A-GENERATED-KEY'
ROUTER='10.99.0.1'
curl -k -X POST "https://$ROUTER/retrieve" \
--form key="$KEY" \
--form data='{"op": "showConfig", "path": ["interfaces", "ethernet", "eth0"]}'
exists is the one that makes idempotent automation
possible: test before you set, and a re-run of the same
playbook produces no commit at all rather than a
no-op commit in the revision history.
What /retrieve does not do is return operational
state. It cannot tell you whether a BGP session is up or
what is in the routing table; it tells you what the
configuration says. That distinction bites hardest in
validation scripts, where “the neighbour is configured”
and “the neighbour is established” are exactly the two
things you must not confuse.
The /show endpoint — operational output
/show runs an operational show command and returns its
output:
KEY='REPLACE-WITH-A-GENERATED-KEY'
ROUTER='10.99.0.1'
curl -k -X POST "https://$ROUTER/show" \
--form key="$KEY" \
--form data='{"op": "show", "path": ["ip", "bgp", "summary"]}'
The data field of the response carries the text a human
would have seen on the terminal, newlines and column
alignment included. It is not structured, and VyOS does
not promise its layout across releases, so a script that
regex-scrapes it is a script that breaks at the next
upgrade.
Where a machine-readable answer exists, prefer it: many
FRR-backed commands accept a json token (show ip bgp summary json), and the JSON output is far more stable
than the aligned text. Where no such form exists, keep the
scraping in one small function with the release it was
written against recorded next to it, so the upgrade that
breaks it has an obvious place to be fixed.
Production deployment
A production HTTP API deployment has three components:
- The router — the API enabled, one named key per
consumer, a listener bound to the management address,
an
allow-clientlist, and a real certificate. - The automation tool — an Ansible playbook, a
Python script, or a configuration-as-code pipeline
that batches its operations into whole-change requests
and calls
/config-filesavewhen the change is confirmed good. - The credential vault — the API keys live in a vault (HashiCorp Vault, Ansible Vault, AWS Secrets Manager), not in the repository and not in plaintext CI environment variables.
Failure modes
The half-applied change
A script sends its operations one request at a time. Request twelve fails validation. Requests one to eleven are committed and live; there is no candidate to discard and no single revision to roll back to.
Diagnostic: show system commit lists a run of commits at
the same timestamp instead of one.
Fix: batch the operations into a single /configure
request so the change is one commit. Where a change is
genuinely too large for one request, make each request a
self-contained, individually safe step, and record the
revision number before you start.
The change that did not survive a reboot
Every change went in through /configure and every
response said success. The router reboots for an
unrelated reason and comes back months out of date.
Diagnostic: compare saved shows a large diff before the
reboot; after it, the running configuration matches an old
config.boot.
Fix: call /config-file with op: save after the change
is confirmed. Treat “committed” and “saved” as two
separate states in the automation, because the router
does.
The key that is everywhere
The key was configured 18 months ago. It has since been in a CI environment variable, in a developer’s shell history, in the monitoring system’s config, and in three config backups. It is not a secret any more.
Diagnostic: the key is in a config archive, or in any document you did not intend to be a credential store.
Fix: rotate on a schedule (90 days is typical, 30 for high-value routers), and rotate immediately whenever a configuration has left your control. Add the new key, switch the consumer, verify, then delete the old one — in that order, so the tool never has a window with no working key.
The listener exposed beyond the management network
listen-address was left at the default and no
allow-client list was configured, so the API answers on
every interface the router has.
Diagnostic: the journal shows authentication failures from
source addresses you do not recognise; show configuration commands | match https shows no listener or client
restriction.
Fix: bind the listener to the management address, add the
allow-client list, and put a firewall rule in front of
it. Traffic addressed to the router itself is filtered by
the input hook, not by forward:
configure
set firewall ipv4 input filter rule 10 action 'accept'
set firewall ipv4 input filter rule 10 protocol 'tcp'
set firewall ipv4 input filter rule 10 destination port '443'
set firewall ipv4 input filter rule 10 source address '10.99.0.0/24'
set firewall ipv4 input filter rule 20 action 'drop'
set firewall ipv4 input filter rule 20 protocol 'tcp'
set firewall ipv4 input filter rule 20 destination port '443'
commit
save
Then rotate the key, because you do not know what the scanner collected.
Rollback
An HTTP API configuration change is reversible through the standard VyOS mechanisms — it is an ordinary commit:
rollback Nandcommitto return to a previous configuration revision.delete service https api keys id automationandcommitto remove one key.delete service https apiandcommitto disable the API entirely.
Two cautions. The commit validator does not know that an
external system depends on this API, so disabling it
produces a silent failure somewhere else rather than an
error here. And if the API is how you reach the router,
the change that breaks the API is also the change you
cannot undo through the API — make it over SSH, or make it
under commit-confirm.
Production discipline
Cross-course references
LIV-VyOS-Automation(vyos-liv-02-vyos-ansible, the next lesson) covers the Ansible integration built on this API.LIV-VyOS-Automation(vyos-liv-03-config-as-code) covers the configuration-as-code pipeline that uses the HTTP API as the deployment target.XLVII-VyOS-MgmtHardening(vyos-xlvii-02-api-auth) covers the API authentication mechanisms in detail.XXXVII-VyOS-Firewallcovers theinputhook used above to filter traffic addressed to the router itself.
Quiz
Knowledge check · 4 questions
Q1. Which VyOS HTTP API endpoint applies configuration-mode operations to the router?
Q2. A successful POST to /configure commits the change to the running configuration, but does not write it to the boot configuration.
Q3. R1 has the HTTP API enabled with a single key and no listener restriction. The journal shows hundreds of failed API authentications per minute from changing source addresses. What is happening and what is the fix?
R1's HTTP API was enabled without setting `service https listen-address` or `service https allow-client address`, so it answers on every interface the router has. The system journal shows hundreds of failed API authentications per minute, from source addresses that change constantly.
Q4. An automation script sends twenty separate /configure requests to build a new BGP peering. The twelfth is rejected by the commit validators. The script aborts and reports a failure. What state is the router in, and how should the script have been written?
A deployment script issues twenty HTTP API `/configure` requests in sequence. Requests one to eleven return `success: true`. Request twelve is rejected by the commit validators and the script aborts. The router is in production and forwarding traffic.
Passing score: 75%. Answers are checked in this browser.