All posts
A cisco opshow-toTOFU

Cisco IOS configuration backup: the complete guide

A 2,500-word guide to backing up Cisco IOS configs the right way — versioned, off-host, encrypted, with restore testing.

Ole OlorentzenJune 30, 202612 min read

Here's the thing: most "config backups" I've seen in production are theatre.

The script runs. The cron fires. Files land in a folder. Everyone relaxes.

Then someone fat-fingers an ACL at 2 AM, restores from "the backup," and discovers it's a 47-byte file. Or it's the right size but from three router reboots ago. Or it's empty because SSH auth broke eleven months ago and nobody noticed.

Having a backup isn't the same as having a backup that works. This guide is about the second one.

What "good" looks like

A Cisco config backup that's actually useful does four things:

  1. Runs reliably — on schedule, with credentials that don't quietly rot
  2. Stores cleanly — versioned, dated, off-host, encrypted
  3. Alerts on failure — silent failure is the #1 cause of "we have backups" being a lie
  4. Has been restored from — if you've never tested the restore, you don't have a backup, you have a file

Everything in this post is in service of those four properties. If a method doesn't get you all four, we'll say so.

What you're actually backing up

Most guides talk about running-config and stop there. That's incomplete. On a Cisco IOS-XE device, the full set you want to capture is:

  • running-config — the active configuration
  • startup-config — what's in NVRAM (what survives reboot)
  • vlan.dat — VLAN database (separate file on most switches)
  • Crypto keys — SSH host keys, SSL certificates, IPsec keys (often stored separately in IOS-XE)
  • Bundle mode info — on IOS-XE, packages/bundle config that determines boot mode

Practically: pull running-config for most use cases. Add startup-config if you want belt-and-suspenders. Capture vlan.dat separately for switches.

If you're doing this for audit evidence (PCI, NIS2, IEC 62443), the running-config plus a timestamp is the minimum. Auditors want to see that you have the configuration as-of a specific date, with a chain of custody.

Related reading: show running-config vs show startup-config: what actually matters

The four methods, honestly compared

MethodWhere it runsProsConsWhen to skip
archive (Cisco)Device pushes to TFTP/FTP/SCPBuilt-in, versioned on deviceHard to scale, device-side scheduling, TFTP/FTP insecure in 2026If you have >10 devices
kron (Cisco)Device-side schedulerRuns commands on a scheduleNo built-in push, you still need TFTP/FTP target, awkward with promptsIf you're pulling externally anyway
External SFTP/SSH pullExternal server (Python/Netmiko)You control the schedule, modern protocols, scales, alerts possibleRequires external server, some PythonIf you don't have a Linux host available
SNMP config-copyNetwork management systemSome NMS tools support itLegacy, slow, vendor-specific, hard to alert onAlmost always in 2026

The honest answer for 2026: external SFTP pull with Python (or a tool built on it) is the right answer for anything past 3-5 devices. The archive command has its place for single critical devices, but it doesn't scale, and it doesn't alert well.

A note on the archive command

archive is genuinely useful and you should know it exists. It's the only Cisco-native method that gives you automatic versioning on the device — every write memory triggers a new archived config. For a single critical router or a regulated environment where you want belt-and-suspenders, it's good.

archive
 path scp://backup@10.0.100.5/configs/$h-config
 write-memory
 time-period 1440

This pushes a new config to the SCP server every 24 hours and every time someone writes memory. Versions accumulate. You can show archive to see history.

The problem: it's device-side. The scheduling is on each device. The alerting is on each device (you'd have to poll). For 50 devices, you're managing 50 schedules. SFTP-pull from a central server is operationally cleaner.

More detail: Cisco archive vs kron: which config backup mechanism wins

The recommended setup: scheduled SFTP pull with versioning

This is what I actually run. Linux host, Python script via cron (or systemd timer), Netmiko for SSH, SFTP for transfer, dated + versioned file storage, email alert on failure.

Step 1: prep the device

On each Cisco IOS-XE device, you need:

! Enable SCP server
ip scp server enable

! Create a backup user with privilege 15
username backup privilege 15 algorithm-type scrypt secret <your-strong-password>

! Ensure SSH v2 (default on 17.x but be explicit)
ip ssh version 2
ip ssh server algorithm authentication keyboard
ip ssh server algorithm authentication publickey

! Lock down VTY — SSH only
line vty 0 15
 transport input ssh

That's the minimum. For production hardening:

  • Restrict the backup user to specific commands via privilege exec level 15 plus a parser view if you want least-privilege
  • Use SSH keys instead of passwords — Netmiko supports both
  • Use aaa authorization exec if you have TACACS+, with a specific backup group

Step 2: prep the backup server

You need:

  • A Linux host (or any Unix-like)
  • Python 3.9+ with netmiko installed (pip install netmiko)
  • An SFTP-receiving directory (/var/backups/cisco/<hostname>/)
  • A cron job or systemd timer
  • An alerting mechanism (SMTP, webhook, etc.)

Step 3: the backup script

Here's the actual script. It backs up a single device; extending to many is trivial (loop over a list).

#!/usr/bin/env python3
"""
Cisco IOS-XE config backup via SFTP-pull using Netmiko.
Target: Cisco IOS-XE 17.x (tested on 17.09.04a)
Tested on: Netmiko 4.x, Python 3.11
"""

import os
import sys
from datetime import datetime
from pathlib import Path
from netmiko import ConnectHandler

# --- Configuration ----------------------------------------------------------
DEVICE = {
    "host": os.environ["DEVICE_HOST"],
    "device_type": "cisco_xe",
    "username": os.environ["DEVICE_USER"],
    "password": os.environ["DEVICE_PASS"],
    "timeout": 30,
}

BACKUP_DIR = Path(os.environ.get("BACKUP_DIR", "/var/backups/cisco"))
RETENTION_DAYS = int(os.environ.get("RETENTION_DAYS", "90"))
# ---------------------------------------------------------------------------


def backup_device():
    """Connect, pull running-config, write versioned file."""
    host = DEVICE["host"]
    today = datetime.now().strftime("%Y%m%d-%H%M%S")
    device_dir = BACKUP_DIR / host
    device_dir.mkdir(parents=True, exist_ok=True)

    outfile = device_dir / f"running-config-{today}.cfg"

    try:
        with ConnectHandler(**DEVICE) as conn:
            # Sanity check: confirm we're on the right device
            prompt_host = conn.find_prompt().strip("#<>")
            if prompt_host != host:
                print(f"WARN: prompt host {prompt_host!r} != expected {host!r}")

            config = conn.send_command("show running-config")

        outfile.write_text(config)
        size = outfile.stat().st_size

        if size < 500:
            # Suspicious — most real configs are at least a few KB
            raise ValueError(f"Config suspiciously small: {size} bytes")

        print(f"OK: {host} -> {outfile} ({size} bytes)")
        return 0

    except Exception as e:
        print(f"FAIL: {host}: {e}", file=sys.stderr)
        return 1


def prune_old():
    """Remove backups older than RETENTION_DAYS."""
    cutoff = datetime.now().timestamp() - (RETENTION_DAYS * 86400)
    removed = 0
    for f in BACKUP_DIR.rglob("running-config-*.cfg"):
        if f.stat().st_mtime < cutoff:
            f.unlink()
            removed += 1
    if removed:
        print(f"Pruned {removed} old backup(s)")


if __name__ == "__main__":
    rc = backup_device()
    if rc == 0:
        prune_old()
    sys.exit(rc)

Run it:

DEVICE_HOST=10.0.1.1 \
DEVICE_USER=backup \
DEVICE_PASS='redacted' \
python3 backup_cisco.py

For multiple devices, wrap it in a loop over a YAML list of devices, or move to nornir if you're going beyond 20-30. For 5-50 devices, a simple bash loop is fine.

Step 4: schedule it

Cron entry, every night at 02:00:

0 2 * * * /opt/cisco-backup/run.sh >> /var/log/cisco-backup.log 2>&1

Or systemd timer if you want proper dependencies and retry logic.

Where to store backups (and how to keep them secure)

Storage layout matters. Here's what I run:

/var/backups/cisco/
├── core-sw-01/
│   ├── running-config-20260620-020001.cfg
│   ├── running-config-20260621-020001.cfg
│   └── ...
├── core-sw-02/
├── access-sw-floor3-01/
└── ...

Per-host directory, dated files. Easy to grep, easy to diff across days, easy to prune.

Three security things to actually do:

  1. Encrypt at rest. If your backup server is compromised, the attacker shouldn't get every device's config in cleartext. gpg or age on the file post-write. Or use filesystem-level encryption (LUKS, ZFS native).
  2. Access control. The backup server is now one of your most sensitive assets. Restrict SSH access. No world-readable directories.
  3. Off-host copy. A backup that lives on the same disk array as the devices it backs up isn't a backup. Push to S3 / Azure Blob / an offsite server nightly. Don't be clever about this; rsync + cron is fine.

For retention: 30 days is the typical minimum. For regulated environments, 1 year. Match your audit requirements, not your disk space.

Compliance framing: IEC 62443-3-3 SR 1.1 (Identification and authentication) and SR 2.1 (Authorization enforcement) both implicitly require configuration baselines you can demonstrate over time. NIS2 Art. 21 requires "appropriate technical and organisational measures" including business continuity — a backup trail is evidence that supports that requirement.

Alerting on backup failure

This is the part nobody builds. The script either runs or it doesn't. If it doesn't, you need to know.

A simple approach: have the script exit non-zero on failure, and have the cron job send mail on non-zero exit.

# /opt/cisco-backup/run.sh
#!/bin/bash
set -e

if ! /opt/cisco-backup/backup_fleet.py; then
    echo "Cisco config backup failed — check /var/log/cisco-backup.log" \
        | mail -s "[ALERT] Cisco backup failure" ops@yourcompany.com
    exit 1
fi

For larger environments: ship the log to your SIEM, alert from there. Even simpler: post to a Slack or Teams webhook on failure.

What to alert on:

  • Script didn't run at all (cron broken, host down)
  • Script ran but returned non-zero (auth failure, command failure)
  • File landed but was suspiciously small (the SSH-broke-11-months-ago scenario — the script catches this with the size check)
  • File landed but the diff vs yesterday is 95% changed (someone wiped the config — could be valid, could be disaster)

The third one is the silent killer. The SSH credential changes, the script keeps "succeeding," and you get an empty file every day until you need it.

The restore test (and why your backup is probably useless)

Here's the part everyone skips. If you've never restored from your backup, you don't actually have a backup. You have a file that might work.

Restore testing is straightforward:

  1. Pick a non-critical device. Test lab is ideal; a maintenance window on something non-critical is fine.
  2. Save current running-config somewhere safe.
  3. Apply the backup file via copy tftp running-config (or equivalent).
  4. Verify the device behaves as expected. Check routing, VLANs, ACLs, BGP sessions.
  5. Restore the original running-config.

For step 4, here's the actual verification list I'd walk through after a restore:

  • Routing: show ip route summary, show ip ospf neighbor, show ip bgp summary — counts and neighbors should match pre-restore
  • Interfaces: show ip interface brief — every interface that was up should still be up
  • ACLs: show ip access-lists — hit counts on critical ACLs should resume (or restart at 0; either way they shouldn't be missing entirely)
  • Spanning tree: show spanning-tree summary — root bridge and port roles should match
  • First-hop redundancy: show standby brief or show vrrp brief — HSRP/VRRP state should match
  • NTP: show ntp status — clock should sync within a minute or two
  • Logging: show logging — syslog source interface should be correct, no flood of errors

If any of those are off, your restore didn't fully work — and now you know, instead of at 2 AM during an outage.

How often: quarterly is reasonable. For regulated environments, before every audit. Document each test — auditors love evidence of tested backups, not just existence of backups. A simple spreadsheet with date, device, who tested, what passed, what failed is enough.

Three things the restore test catches that nothing else does:

  • Backup file is from the wrong device (hostname doesn't match)
  • Backup file is missing critical pieces (crypto keys, certificates)
  • Restore syntax on the device doesn't match the backup's syntax (different IOS version)

The third one bites hard in mixed-version environments. If you backed up an IOS-XE 16.x config and try to merge it into a 17.x device, certain feature syntax silently gets dropped. The restore "succeeds" but the config isn't what you think it is.

Related: Why your Cisco configs drift (and what to do about it)

Common gotchas (the things that bit me)

A few things that aren't in any of the docs:

  • Hostname in prompt vs hostname in config. IOS-XE's prompt defaults to the hostname, but if someone changed the prompt, your sanity check (prompt_host != host) will fire. Worth knowing — don't blindly trust it.
  • Timezone on the backup server. If your server is in UTC but your filenames use local time, midnight UTC backups can land in the wrong "day." Pick one and stick to it.
  • write memory vs copy running-config startup-config. Same thing, but if your script uses one and Cisco changes the syntax in a future release, you're stuck. Don't rely on either — check both in your post-backup verification.
  • TFTP works but TFTP is not backup. TFTP has no auth, no encryption, and no integrity check. If your backup server is on the same VLAN as your user network, anyone with a packet capture gets every config. Use SFTP/SCP. Always.
  • archive keeps versions on the device. That sounds great until your device runs out of flash. Set a max with the maximum subcommand under archive config, or you'll fill up flash and crash the device.
  • Your backup is only as good as your credentials. Rotate them. The Python script above reads from env vars — set them in /etc/default/cisco-backup with chmod 600, not in the script body.
  • Don't back up during a config change in progress. If someone is mid-edit when your backup fires, you get a half-applied config. Schedule for low-traffic windows.

Going further — full fleet automation

Once you have one device working, scaling to 50 is a matter of:

  • Putting device lists in YAML (host, type, credential source)
  • Running backup jobs in parallel (Netmiko has a thread pool, or use nornir for cleaner concurrency)
  • Centralizing logs and alerts
  • Adding a diff step: after backup, diff against yesterday's. If the diff is large, post it somewhere you can review.

This is what PacketPilot Config does — it wraps the pattern above for multi-vendor fleets (Cisco IOS/NX-OS, Juniper JunOS, Arista EOS, HPE/Aruba), with scheduled daily backups, diff-style compare between any two backups, restore any previous version, and a timestamped audit trail of every change. Self-hosted Windows desktop app — your credentials never leave the laptop. You don't need it for 5 devices. For 50+, or if audit prep eats your weekends, it's the difference between a tool you wrote and a tool someone maintains.

Try it

If you've been running config backups from a cron job you wrote in 2019 and praying — there's a faster way. PacketPilot Config runs the same pattern out of the box: scheduled backups, diff-style compare, one-click restore, full audit trail. Multi-vendor from day one. 30-day machine-bound trial — no credit card, no email gating, just download and run.

If you want to follow what we're building, find dB-Electronics on LinkedIn. No spam — just product updates and the occasional technical post.

— Ole