Runbook: Respond to Compromised Router Credentials
1 · Prerequisites
Confirm every item is in place before any state change.
- SSH hardening — key-only auth, port, listen-address, mgmt VRF
- HTTP API authentication — keys, localhost binding, TLS, the gRPC alternative
- Source restrictions — listen-address, allow-client, and the firewall input chain
- Out-of-band access — OOB management VRF, console server, IPMI, the lockout safety net
- User roles — RBAC, privilege levels, the role-per-concern pattern
- PKI and certificate rotation — x509, ACME, Let's Encrypt, the rotation schedule
- Post-install hardening — what to change before the box touches the public Internet
- commit-confirm — the rollback safety net for remote changes
- Configuration history — the archive of every successful commit
- Remote backup — cron + rsync, version control commit, S3 bucket
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-keysentry name, or an API keyid. "Someone has SSH access" is not a containment target.show system login userandshow configuration commands | match "service https api keys"produce the two lists - · The role bound to the credential is READ, not assumed:
show system login userandshow 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 commithas 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-confirmtimer 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.
- 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.
- 2Classify the credential by what its role could reach, using
show system login userandshow system login role. A role that permitsshowpermitsshow configuration, which is the whole secret inventory - so a "read-only" credential is not a small incident. - 3Capture the evidence containment destroys, in this order: the central syslog copy of the authentication record, then
show log authon the router, thenshow configuration commit, then a fullshow configuration commandsdump written off-box. Rotation and rollback overwrite the last two. - 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.
- 5Contain the PATH before the credential where a path exists:
set service ssh source-restrict ip-addressfor the operator prefixes, or the management-plane firewall rule set. This shuts the door without destroying the account that is also the evidence. - 6Disable the account rather than deleting it:
set system login user NAME role 'disable', applied withcommit-confirm. The disable role blocks authentication and preserves the user-to-audit-entry link that deletion severs. - 7Remove the authentication material explicitly:
delete system login user NAME authentication public-keys KEYNAMEand anyplaintext-passwordnode. A disabled role is a policy; the key is still a credential and may be honoured elsewhere. - 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. - 9Run
confirmbefore the timer expires, thensave. An unconfirmed containment auto-reverts, and an unsaved one reverts at the next reboot; both restore the compromised credential without anyone deciding to. - 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.
- 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. - 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 usershows the account withrole disableand nopublic-keysorplaintext-passwordnode 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 authsince 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 summaryback to Established with the new password,show ip ospf neighborback to Full,show vpn ipsec saup, and the WireGuard peer showing a recent handshake - ✓
show configuration commitshows the containment commit, theconfirmthat 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-confirmtimer 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-restrictlist 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 1is 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 disableis 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 commitholds 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
vyosaccount 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
vyosbreak-glass account - is known or suspected to be in the wrong hands. - An API key configured under
service https api keyshas 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 author 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-lockoutterritory - 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-routerwith 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
# 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 statusNot “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?
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 permits | What the incident actually is |
|---|---|
A custom role listing exact show commands | Disclosure bounded by that list |
Any role permitting bare show | Disclosure of the entire configuration, including every shared secret |
operator | The above, plus service restarts - availability as well as disclosure |
admin | The above, plus commits. Treat the configuration as potentially altered, not merely read |
| An API key bound to a user | Whatever that user’s role permits, over HTTPS, without a shell |
Step 2: capture what containment destroys
# 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.
show log auth | match "Accepted"
show log auth | match "Failed"
show log auth | match "vyos-http-api"
show configuration commitThree 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
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
exitThe 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.
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
exitBoth 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.
configure
delete service https api keys id ansible-prod
compare
commit-confirm 10
exitKey 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.
configure
confirm
save
exitconfirm 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.
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"| Secret | Where it lives | What rotating it costs |
|---|---|---|
| BGP session password | protocols bgp neighbor ... password | A coordinated change with the peer AS; the session drops if only one side changes |
| OSPF key material | protocols ospf interface ... authentication | Adjacency-wide; every router in the area must change together |
| IPsec pre-shared secret | vpn ipsec site-to-site peer ... authentication pre-shared-secret | Tunnel down until both ends agree the new value |
| WireGuard private key | interfaces wireguard ... private-key | The public key changes with it, so every peer must be updated |
| SNMP community | service snmp community | Monitoring blind until the poller is updated |
| Remaining API keys | service https api keys id ... key | Each consumer must be updated; do them one at a time |
| TLS key material | the pki tree | Re-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
show configuration commit
show system config-managementRecord 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
# 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:
show system login user
show log auth | match "Accepted"
show configuration commit
diff /config/config.boot <(show configuration commands)Common patterns
| Symptom | Likely cause | Resolution |
|---|---|---|
| Containment reverted itself after ten minutes | commit-confirm timer expired unconfirmed | Re-apply, then confirm and save in the same session |
| Containment gone after a reboot | confirm ran, save did not | Re-apply and save; verify the running and saved configurations agree |
| Compromised credential works again weeks later | A restore or rollback reintroduced it | Re-apply the rotations; the archive and the repository are contaminated by design |
| Account disabled, key still accepted elsewhere | The public-keys entry was left in place, and the key is installed on sibling routers | Delete the key entry here and audit the fleet for the same key |
| Audit entries now reference an unknown user | The account was deleted rather than disabled | Not recoverable. Use role disable next time |
| BGP session dropped during rotation | Only one side of a two-party secret changed | Coordinate the change with the peer; verify with show ip bgp summary |
| Monitoring went blind after containment | The SNMP community or the monitoring role was rotated without updating the poller | Rotate service credentials with their consumer, one at a time |
| Operator locked out along with the attacker | source-restrict omitted the current source address | Correct the list over OOB; do not remove the restriction wholesale |
Nothing in show log auth, but the API was used | HTTP API activity is logged under vyos-http-api, not as an SSH login | Search the API lines and the central syslog copy separately |
| An archive entry nobody can attribute | The credential had write access | Stop. 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 commitshows an entry nobody can account for. - Out-of-band access cannot be established.
- The credential in question is the break-glass
vyosaccount 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.