Skip to main content
RunBook Academy

VyOSLIV · API and AutomationAutomation

VyOS HTTP API — the /configure endpoint, key authentication, and one commit per request

Advanced⏱ ~26 mincurlset service https api keysset service https listen-addressset service https allow-client addressshow configuration commandsjournalctl

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)

Not yet marked complete on this device.

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:

  • /configure takes configuration-mode operations — set, delete, comment — and applies them.
  • /config-file takes save and load: writing the running configuration to disk, or loading a configuration file.
  • /retrieve reads the configuration tree (showConfig, returnValue, returnValues, exists). It is not an operational-state endpoint.
  • /show runs an operational show command 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 under service https api. listen-address and port are 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 address is 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 automation is 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, and strict). Type set 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:

  1. The router — the API enabled, one named key per consumer, a listener bound to the management address, an allow-client list, and a real certificate.
  2. 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-file save when the change is confirmed good.
  3. 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 N and commit to return to a previous configuration revision.
  • delete service https api keys id automation and commit to remove one key.
  • delete service https api and commit to 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-Firewall covers the input hook used above to filter traffic addressed to the router itself.

Quiz

Knowledge check · 4 questions

  1. Q1. Which VyOS HTTP API endpoint applies configuration-mode operations to the router?

  2. Q2. A successful POST to /configure commits the change to the running configuration, but does not write it to the boot configuration.

  3. 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.

  4. 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.