Skip to main content
RunBook Academy

← All runbooks in VyOS

critical risksecurity relevant~90 min

Runbook: Respond to Compromised Router Credentials

1 · Prerequisites

Confirm every item is in place before any state change.

2 · Pre-checks

Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.

  • · Out-of-band access is not merely documented but tested, right now, by logging in over it: console server, serial, or IPMI. Every containment action in this runbook can remove the path the operator is currently connected over
  • · The security owner is named and the incident is open BEFORE the first containment commit. A credential incident has an external audience - peers whose shared secrets are in this configuration - and a notification clock that the on-call operator does not own
  • · The credential is identified exactly: a username, a public-keys entry name, or an API key id. "Someone has SSH access" is not a containment target. show system login user and show configuration commands | match "service https api keys" produce the two lists
  • · The role bound to the credential is READ, not assumed: show system login user and show system login role. The role decides whether this is a disclosure incident, a change incident, or both, and the three have different first moves
  • · The authentication record is pulled from the CENTRAL syslog copy first, not from the router. Local logs are inside the blast radius of anything with write access; the exported copy is the one that can be trusted
  • · show configuration commit has been read and every entry attributed to a known operator, time and source address. An entry nobody recognises moves this from an access incident to a change incident, and the rest of the ordering changes with it
  • · A decision has been taken on whether the automation holding this credential can survive its rotation. If the answer is no, HOLD is a legitimate state with a named owner and an end time - not an omission
  • · The commit-confirm timer length is agreed before the first commit, and the operator has agreed not to leave the session while it runs. Here the auto-rollback undoes containment rather than a mistake

3 · Procedure

Execute each step in order. Verify the expected output of a step before moving to the next.

  1. 1Establish and TEST out-of-band access before touching anything. Containment on the management plane removes management-plane access; that is the point of it, and it applies to the operator too.
  2. 2Classify the credential by what its role could reach, using show system login user and show system login role. A role that permits show permits show configuration, which is the whole secret inventory - so a "read-only" credential is not a small incident.
  3. 3Capture the evidence containment destroys, in this order: the central syslog copy of the authentication record, then show log auth on the router, then show configuration commit, then a full show configuration commands dump written off-box. Rotation and rollback overwrite the last two.
  4. 4Establish whether the credential was USED, and whether it CHANGED anything. Failed attempts, a successful login, and a commit in the archive attributed to that user are three different incidents in ascending order of cost.
  5. 5Contain the PATH before the credential where a path exists: set service ssh source-restrict ip-address for the operator prefixes, or the management-plane firewall rule set. This shuts the door without destroying the account that is also the evidence.
  6. 6Disable the account rather than deleting it: set system login user NAME role 'disable', applied with commit-confirm. The disable role blocks authentication and preserves the user-to-audit-entry link that deletion severs.
  7. 7Remove the authentication material explicitly: delete system login user NAME authentication public-keys KEYNAME and any plaintext-password node. A disabled role is a policy; the key is still a credential and may be honoured elsewhere.
  8. 8Revoke a compromised API key by its id - delete service https api keys id NAME - rather than disabling the whole HTTPS service. Granular key ids exist so that revocation does not take the automation down with the attacker.
  9. 9Run confirm before the timer expires, then save. An unconfirmed containment auto-reverts, and an unsaved one reverts at the next reboot; both restore the compromised credential without anyone deciding to.
  10. 10Inventory every shared secret this credential could have read - BGP passwords, OSPF key material, IPsec pre-shared secrets, WireGuard private keys, SNMP communities, remaining API keys - and rotate each one WITH its peer, one at a time, each with its own verification.
  11. 11Neutralise the contaminated recovery path: the commit archive, /config/backups/, the off-box rsync copy and the Git history all contain the compromised secrets. Record which entries are unusable as restore sources before anyone reaches for one.
  12. 12Verify from a client that holds the compromised credential that it now fails, then write the timeline: first use, detection, containment, rotation complete. The gap between the first two and the gap between the last two have different fixes.

4 · Verification

Confirm the procedure actually fixed the problem.

  • show system login user shows the account with role disable and no public-keys or plaintext-password node remaining under it
  • A client still holding the compromised key or password is rejected - tested from outside the router, not inferred from the configuration
  • show configuration commands | match "service https api keys" no longer lists the revoked key id, and the consumer that used it reports 401 rather than silence
  • show log auth since the containment commit contains no successful authentication for the account, and the central syslog copy agrees with the local one
  • Each rotated secret is verified on the protocol rather than in the configuration: show ip bgp summary back to Established with the new password, show ip ospf neighbor back to Full, show vpn ipsec sa up, and the WireGuard peer showing a recent handshake
  • show configuration commit shows the containment commit, the confirm that kept it, and no entry the team cannot attribute
  • The running and saved configurations agree, proving the containment survives a reboot: diff /config/config.boot <(show configuration commands) returns nothing
  • The set of archive entries, backup files and Git commits that still contain the compromised secrets is written down and attached to the incident, so no later restore silently reintroduces them

5 · Rollback

If verification fails, undo the procedure in reverse order.

  • While the commit-confirm timer runs, doing nothing reverts the change. In every other runbook that is the safety net; here it is the hazard, because what reverts is the containment. Set the timer for the verification you actually intend to run and stay in the session
  • A containment that locked out a legitimate operator is corrected by editing the source-restrict list or the role node and committing again - over the out-of-band path. Do not remove the containment wholesale to restore one person access
  • rollback 1 is NOT the undo for this runbook. The previous archive entry is by definition the configuration that still contains the compromised credential, so rolling back restores it and does so without anyone noticing
  • The same applies to a restore: load /config/backups/..., the off-box rsync copy and the Git history all predate the rotation. A restore during or after this incident must be followed by re-applying every rotation, or it is a re-compromise
  • POINT OF NO RETURN: a secret that was readable has been read. Rotation bounds the future; it does not undo the disclosure, and the configuration is where BGP passwords, IPsec pre-shared secrets and WireGuard private keys live
  • POINT OF NO RETURN: delete system login user NAME. It removes the account, its keys and the link between the audit entries and a configured identity in a single commit. role disable is equally effective at blocking login and keeps the evidence
  • POINT OF NO RETURN: traffic already carried under a leaked pre-shared secret or WireGuard key. Rekeying protects the next session, not the captured ones
  • A rotated routing-protocol secret cannot be reverted unilaterally. The peer changed at the same time, so reverting one side alone drops the adjacency rather than restoring it

6 · Escalation

When the runbook isn't enough, contact:

  • · Escalate to the security owner before the first containment commit, not after it. Containment destroys evidence, and whether to preserve or to stop the bleeding is their decision, not the on-call operator to make alone
  • · Escalate to every peer whose shared secret is in this configuration. A BGP password, an OSPF key or an IPsec pre-shared secret is a two-party credential; rotating your half without theirs takes the adjacency down instead of securing it
  • · Escalate to network engineering before source-restricting or firewalling the management plane of a router in a redundant pair. Losing management access to one half of a pair during an incident turns a security event into an availability event
  • · Escalate immediately if show configuration commit holds an entry nobody can attribute. That is a change incident: the question is no longer what was read but what was altered, and the configuration can no longer be assumed intact
  • · Escalate to the platform team if out-of-band access cannot be established. Containing the management plane without it risks an unrecoverable lockout, and the correct answer may be to wait for OOB rather than to contain now
  • · Escalate before touching the break-glass vyos account or the API key the automation pipeline authenticates with. Either rotation can remove the ability to push the remaining fixes
  • · Escalate beyond this router if the credential was reused - the same key or password on sibling routers, on the backup host, or on the configuration repository. One compromised router credential with reuse is a fleet incident

“A router credential has been compromised” describes at least four incidents that share a sentence and very little else: a password or key that authenticates an interactive login, an API key that authenticates automation, a community string or service account with no human behind it, and a break-glass account whose whole purpose is to work when nothing else does. The containment differs for each, and so does what you are allowed to break while containing it.

What they have in common is the thing that makes this runbook different from its equivalent on a server. A VyOS router’s configuration is a secret store. BGP passwords, OSPF key material, IPsec pre-shared secrets, WireGuard private keys, SNMP communities and the API keys themselves all live in the configuration tree, and any credential that can run show configuration has read all of them. That is why the first question is not “what did they do” but “what could they see” - and why a credential you would describe as read-only can be the more expensive incident, because it is the one whose blast radius includes other people’s routers.

The second thing that makes this runbook different is that the ordinary recovery tools are contaminated. rollback 1 restores the previous archive entry, and the previous archive entry contains the compromised credential. So does every backup file, every hourly rsync copy and every commit in the configuration repository. In every other VyOS incident, “go back to the last-known-good” is the fast answer. Here it is how the compromise comes back.

When to use this runbook

  • A router login credential - password, SSH key, or the vyos break-glass account - is known or suspected to be in the wrong hands.
  • An API key configured under service https api keys has leaked: pasted into a ticket, committed to a repository, or held by a host that has itself been compromised.
  • A workstation, laptop or automation host that held router credentials has been lost or breached.
  • An operator has left, or changed role, under circumstances that make an orderly deprovisioning inappropriate.
  • show log auth or the central syslog stream shows a successful authentication that no operator claims.

When not to use it

  • The credential is being rotated on schedule with nothing suspected. That is a planned change: vyos-rb-safe-remote-change, at change-window pace, with a peer review.
  • Nobody can log in and no compromise is suspected. A lockout after a management-plane change is vyos-bf-remote-commit-lockout territory - the fix is to undo the change, and rollback is safe there because the credential is not the problem.
  • The configuration is known-bad but not disclosed. Use vyos-rb-rollback-bad-config, which can use the archive freely.
  • The router itself is suspected of having been altered, not merely read. That is a rebuild decision, not a rotation: it runs through vyos-rb-recover-failed-router with the configuration treated as untrusted input.

Blast radius

Two circles, and operators routinely see only the inner one.

The inner circle is this router: who can log in, what the automation can still do, and whether the management plane stays reachable while you work. Every containment action in this runbook narrows management access, which is exactly what it is for, and which is why Step 0 exists.

The outer circle is every party that shares a secret with this router. A BGP password is agreed with a peer. An OSPF key is agreed across an area. An IPsec pre-shared secret is agreed with the far end. A WireGuard private key corresponds to a public key held by peers. Rotating any of these is a coordinated change with somebody else’s maintenance window attached, and none of them is optional once the configuration has been read.

Step 0: out-of-band first, and prove it works

Read-only / Safetest the path you will need, before you need it
# Substitute your own values before running:
CONSOLE=console-server.example.com
CONSOLE_PORT=4001
BMC=192.0.2.50

ssh "$CONSOLE" -p "$CONSOLE_PORT"

ipmitool -I lanplus -H "$BMC" -U admin power status

Not “we have a console server”. Log in over it now, from the workstation you will be using, and leave the session open. Containment on the management plane is indistinguishable, from the router’s point of view, from an attack on the management plane: a source-restrict list that omits your current source address, or a role change applied to the wrong account, locks out the operator just as effectively as the attacker.

Step 1: what could this credential do?

Read-only / Safethe two inventories
show system login user
show system login role

show configuration commands | match "system login"
show configuration commands | match "service https api keys"
show configuration commands | match "service snmp"

Read the role; do not assume it. The RBAC lesson’s blast-radius model is the one to apply here: a monitor or custom read-only role means the attacker can read system state, an operator role adds restarting services, and admin means they could edit configuration, commit, save and lock everyone else out.

What the role permitsWhat the incident actually is
A custom role listing exact show commandsDisclosure bounded by that list
Any role permitting bare showDisclosure of the entire configuration, including every shared secret
operatorThe above, plus service restarts - availability as well as disclosure
adminThe above, plus commits. Treat the configuration as potentially altered, not merely read
An API key bound to a userWhatever that user’s role permits, over HTTPS, without a shell

Step 2: capture what containment destroys

Read-only / Safethe central copy first, then the router
# Substitute your own values before running:
ROUTER=edge-01.example.com
SYSLOG_HOST=syslog.example.com
CASE=/srv/incident/CASE-1234

mkdir -p "$CASE"

# 1. The exported authentication record. This one is outside the blast radius.
ssh "$SYSLOG_HOST" "grep -h '$ROUTER' /var/log/remote/*.log" > "$CASE/auth-central.log"

# 2. The router's own view, for comparison. A divergence is itself evidence.
ssh "vyos@$ROUTER" 'show log auth' > "$CASE/auth-router.log"

# 3. The commit record: who committed what, when, from where.
ssh "vyos@$ROUTER" 'show configuration commit' > "$CASE/commits.txt"

# 4. The configuration as it stands, before you change it.
ssh "vyos@$ROUTER" 'show configuration commands' > "$CASE/config-pre.txt"

Order matters. The central syslog copy comes first because it is the only one the compromised credential could not have touched - the local logging lesson’s whole argument for exporting is this moment. Compare the two afterwards: if the router’s local record is shorter than the central one, that difference is a finding, not a glitch.

show configuration commit is the one operators forget, and it is the record this runbook turns on. It carries the commit number, timestamp, user and source address for every archived commit. Read it line by line and attribute every entry. One unattributable line changes the incident: you are no longer asking what was disclosed, you are asking what was changed, and the configuration in front of you can no longer be treated as yours.

Read-only / Safedid the credential get used, and did it change anything
show log auth | match "Accepted"
show log auth | match "Failed"
show log auth | match "vyos-http-api"
show configuration commit

Three findings, in ascending order of cost. Failed attempts only: the credential is being tried, not held - contain and rotate at your own pace. A successful authentication: it is held; disclosure has happened. A commit in the archive attributed to that user: it is held and it was used to change the router, and everything after this point runs alongside a change investigation.

Step 3: contain the path before the credential

Service impact possiblepath containment - note the timer
configure
set service ssh source-restrict ip-address '203.0.113.50'
set service ssh source-restrict ip-address '203.0.113.51'
compare
commit-confirm 10
exit

The peer runbooks treat the commit-confirm timer as the mechanism that saves you from your own mistake. Here it saves you from a lockout and simultaneously threatens the containment, because what expires is the security control. Set the timer to cover the verification you actually intend to run, stay in the session, and go straight to confirm when the check passes.

Service impact possibleaccount containment - disable, do not delete
configure
set system login user compromised-operator role 'disable'
delete system login user compromised-operator authentication public-keys laptop
delete system login user compromised-operator authentication plaintext-password
compare
commit-confirm 10
exit

Both halves are needed and they do different things. The disable role blocks authentication as policy; deleting the public-keys entry removes the credential itself, which matters because the same key may be installed on other routers and because a later role edit could re-enable an account whose key was left in place.

Service impact possiblerevoke one API key, not the API
configure
delete service https api keys id ansible-prod
compare
commit-confirm 10
exit

Key ids exist so that revocation can be granular. Disabling the HTTPS service outright takes down every consumer, including the pipeline you may need to push the remaining fixes. Revoke the id that leaked; leave the others answering.

Configuration changekeep the containment
configure
confirm
save
exit

confirm before the timer, save after it. A containment that is not confirmed reverts on a timer; one that is not saved reverts at the next reboot. Both failures restore the compromised credential without a human deciding to, and both are invisible until someone uses it.

Step 4: the secret inventory nobody wants to do

If Step 1 established that the credential could run show configuration, then disabling the account is the beginning of the response and not the end of it. Everything in the tree that is a secret has been read.

Read-only / Safeenumerate what was exposed
show configuration commands | match "password"
show configuration commands | match "authentication"
show configuration commands | match "pre-shared-secret"
show configuration commands | match "wireguard"
show configuration commands | match "service snmp"
show configuration commands | match "service https api keys"
SecretWhere it livesWhat rotating it costs
BGP session passwordprotocols bgp neighbor ... passwordA coordinated change with the peer AS; the session drops if only one side changes
OSPF key materialprotocols ospf interface ... authenticationAdjacency-wide; every router in the area must change together
IPsec pre-shared secretvpn ipsec site-to-site peer ... authentication pre-shared-secretTunnel down until both ends agree the new value
WireGuard private keyinterfaces wireguard ... private-keyThe public key changes with it, so every peer must be updated
SNMP communityservice snmp communityMonitoring blind until the poller is updated
Remaining API keysservice https api keys id ... keyEach consumer must be updated; do them one at a time
TLS key materialthe pki treeRe-issue rather than re-use; the CA question belongs to the PKI owner

Rotate one at a time, each with its own verification, and verify on the protocol rather than in the configuration. A BGP password that is present in show configuration commands and a BGP session that is Established are different claims, and only the second one means the rotation worked.

Step 5: the recovery path is contaminated

Read-only / Safemark the contaminated restore sources
show configuration commit
show system config-management

Record which archive entries, which backup files and which repository commits predate the rotation, and attach that list to the incident. The person who restores this router in six months will not be the person who ran this runbook, and the archive gives them no signal at all that entry 7 carries a credential somebody else knows.

Step 6: verify from outside

Read-only / Safeprove the credential is dead
# Substitute your own values before running:
ROUTER=edge-01.example.com
OLD_KEY=~/.ssh/compromised_ed25519
OLD_API_KEY=the-leaked-key-string

# Must fail.
ssh -i "$OLD_KEY" -o IdentitiesOnly=yes "compromised-operator@$ROUTER" 'show version'

# Must return 401, not a result.
curl -k -s -o /dev/null -w '%{http_code}' -H "X-API-Key: $OLD_API_KEY" "https://$ROUTER:8443/show/version"

Test the negative. A configuration that says the account is disabled and an authentication attempt that is refused are different claims, and only the second one is evidence. Then confirm the containment persists:

Read-only / Safeand prove it survives a reboot
show system login user
show log auth | match "Accepted"
show configuration commit
diff /config/config.boot <(show configuration commands)

Common patterns

SymptomLikely causeResolution
Containment reverted itself after ten minutescommit-confirm timer expired unconfirmedRe-apply, then confirm and save in the same session
Containment gone after a rebootconfirm ran, save did notRe-apply and save; verify the running and saved configurations agree
Compromised credential works again weeks laterA restore or rollback reintroduced itRe-apply the rotations; the archive and the repository are contaminated by design
Account disabled, key still accepted elsewhereThe public-keys entry was left in place, and the key is installed on sibling routersDelete the key entry here and audit the fleet for the same key
Audit entries now reference an unknown userThe account was deleted rather than disabledNot recoverable. Use role disable next time
BGP session dropped during rotationOnly one side of a two-party secret changedCoordinate the change with the peer; verify with show ip bgp summary
Monitoring went blind after containmentThe SNMP community or the monitoring role was rotated without updating the pollerRotate service credentials with their consumer, one at a time
Operator locked out along with the attackersource-restrict omitted the current source addressCorrect the list over OOB; do not remove the restriction wholesale
Nothing in show log auth, but the API was usedHTTP API activity is logged under vyos-http-api, not as an SSH loginSearch the API lines and the central syslog copy separately
An archive entry nobody can attributeThe credential had write accessStop. This is a change incident; the configuration is untrusted input

Escalation

Escalate when:

  • Before the first containment commit - to the security owner, because containment destroys evidence.
  • To every peer holding a shared secret that appears in this configuration.
  • Before restricting the management plane on one half of a redundant pair.
  • The moment show configuration commit shows an entry nobody can account for.
  • Out-of-band access cannot be established.
  • The credential in question is the break-glass vyos account or the API key the automation pipeline depends on.
  • There is any sign of reuse on another router, the backup host, or the configuration repository.

References

  1. VyOS documentation - user accounts and login
  2. VyOS documentation - SSH service
  3. VyOS documentation - HTTPS API service
  4. VyOS documentation - PKI
  5. VyOS documentation - commit-confirm
  6. VyOS documentation - syslog
  7. NIST SP 800-53 - AC-6 Least Privilege