Cisco ASA Multicontext Firewall Backup
Auf der Cisco ASA Firewall wird ein Backup-Benutzer benötigt, der minimale Privilegien in den Kontexten admin und system benötigt. Der öffentliche SSH Authentisierungsschlüssel ist jener vom Benutzer auf dem Backup-Server unter welchem das Script aufgerufen wird.
changeto context admin privilege cmd level 6 command changeto enable password ********** level 6 username backup privilege 6 username backup attributes ssh authentication publickey ThEeNcRyPtEdSsHpUbLiCkEy== changeto system privilege cmd level 6 command backup privilege cmd level 6 mode exec command copy privilege show level 6 mode exec command tech-support
Leider unterstützt Cisco ASA keine Authentisierung mittels öffentlichem Schlüssel für scp-Befehle von der Firewall auf einen Backup-Server. Deshalb muss das Passwort auf der Kommandozeile im Klartext angegeben werden. Welch eine Schande für eine Firma, welche Sicherheitsgeräte verkauft.
Untenstehend das Expect-Skript, welches via SSH auf die Cisco ASA Firewall verbindet, in den privilegierten Modus wechselt, Tech-Support Informationen holt und ein Backup macht. Wegen eines Bugs auf der ASA Firewalls, muss dieses zuerst auf die Flash-Disk geschrieben werden und dann mittels copy-Befehl über scp auf den Backup-Server kopiert werden. Expect verwendet die Tcl-Syntax.
#!/usr/bin/expect -f # ============================================================================ # Backup of Cisco ASA firewalls with multiple contexts. # ============================================================================ exp_internal 0 log_user 1 # ---------------------------------------------------------------------------- # Constants # ---------------------------------------------------------------------------- set ASA_USER "backup" set ASA_ENABLE 6 set BACKUP_USER "ciscobackup" set BACKUP_HOST "123.123.123.123" set PASSWORD_FILE "$::env(HOME)/.backuppw" # ---------------------------------------------------------------------------- # abort # ---------------------------------------------------------------------------- # Prints the string given as argument out to stderr and exits. # proc abort {message } { puts stderr "ABORT: ${message}\n" exit 1 } # ---------------------------------------------------------------------------- # sanitize_hostname # ---------------------------------------------------------------------------- # Removes every non-allowed character from string. Keep shell argument safe. # proc sanitize_hostname {string1} { set string2 "" regsub -all -- {[^A-Za-z0-9\.\-]+} $string1 {} string2 return $string2 } # ---------------------------------------------------------------------------- # sanitize_dir # ---------------------------------------------------------------------------- # Removes every non-allowed character from string. Keep shell argument safe. # proc sanitize_dir {string1} { set string2 "" regsub -all -- {[^A-Za-z0-9_/\.\-]+} $string1 {} string2 return $string2 } # ---------------------------------------------------------------------------- # get_retention_suffix # ---------------------------------------------------------------------------- # Define retention filename suffix for simple retention algorithm: # Daily up to 7 per week # Monthly up to 12 per year on 1st day of month. # Yearly up to forever on 1st of January # Run script daily after midnight. # proc get_retention_suffix {} { set year [timestamp -format %Y] set month [timestamp -format %m] set day [timestamp -format %d] set weekday [timestamp -format %w] if {$day == 1 && $month == 1} { set suffix "yearly_${year}" } elseif {$day == 1} { set suffix "monthly_${month}" } else { set suffix "daily_${weekday}" } return $suffix } # ---------------------------------------------------------------------------- # read_password # ---------------------------------------------------------------------------- # Reads passwords from file defined in $PASSWORD_FILE given as argument. Trims # any trailing newlines and spaces. # proc read_password {pwfile} { set password "" if {![file exist $pwfile]} { abort "Password file $pwfile does not exist!" } else { set fp [open $pwfile r] set data [read $fp] close $fp regsub {[\n\r\t ]+$} $data {} password } return $password } # ---------------------------------------------------------------------------- # connect_firewall # ---------------------------------------------------------------------------- # Connects via SSH to firewall. Accept new host key if necessary. Then enable # privileged exec mode. # proc connect_firewall {asa_user asa_host asa_enable password} { global prompt global spawn_id # Connect via SSH to firewall. Accept new host key if necessary. send_user -- "Connecting to ${asa_user}@${asa_host} ...\n" spawn ssh "${asa_user}@${asa_host}" expect { -re {The authenticity of host .* can.t be established.} {exp_continue} -re {RSA key fingerprint is} {exp_continue} -re {Are you sure you want to continue connecting \(yes/no\)\?} { send -- "yes\r"; exp_continue } -re {Warning: Permanently added .* to the list of known hosts.} { exp_continue } -re {User .* logged in to} { exp_continue } -re $prompt {} timeout { abort "Timeout at ssh login" } default { abort "Unknown condition occured at ssh login." } } # Enable privileged exec mode. send_user -- "Enable privileged exec mode ...\n" send -- "enable ${asa_enable}\r" expect { "Password: " {} "No password set for privilege level" { exit 1 } default { abort "Unknown condition occured at enable." } } send -- "${password}\r" expect { default { abort "Unknown condition occured at enable password." } timeout { abort "Timeout occured at enable password." } "Invalid password" { abort "Invalid password" } "Access denied" { abort "Access denied" } -re $prompt {} } expect -re {.*} # Flush buffer return } # ---------------------------------------------------------------------------- # get_version # ---------------------------------------------------------------------------- # Returns the ASA software version as floating point decimal number. # Cisco ASA version numbering is major.minor, which means that 9.12 is # greather than 9.3 for example. Convert to decimal for mathematical # comparison. 9.12 becomes 9.012 and 9.3 becomes 9.003 for example. # A thousand minor versions should be enough. :-) # proc get_version {} { global prompt global spawn_id set version_major "" set version_minor "" expect -re {.*} # Flush buffer send_user -- "Checking for ASA version ..." send -- "show version | include ^Cisco.*Appliance.*Version\r" expect { default { abort "Unknown condition occured at show version" } timeout { abort "Timeout occured at show version." } -re {Cisco.*Appliance Software Version ([0-9]+)\.([0-9]+)} { set version_major $expect_out(1,string) set version_minor $expect_out(2,string) } -re $prompt {} } set version_float [expr { $version_major + $version_minor/1000.0}] expect -re {.*} # Flush buffer return $version_float } # ---------------------------------------------------------------------------- # get_context_mode # ---------------------------------------------------------------------------- # Queries context mode of the ASA firewall. Change to system context if ASA # has multiple contexts. Returns context mode (single, multiple). # proc get_context_mode {} { global prompt global spawn_id set context_mode "" send_user -- "Checking context mode ...\n" send -- "show mode\r" expect { -re {context mode: single} {set context_mode "single"} -re {context mode: multiple} {set context_mode "multiple"} -re $prompt {} timeout { abort "Timeout at 'show mode'" } default { abort "Unknown condition occured at 'show mode'" } } if {$context_mode eq "multiple"} { send_user -- "Multiple contexts: Changing to system context ...\n" send -- "changeto system\r" expect { -re $prompt {} default { abort "Unknown condition occured at 'changeto system'" } } } expect -re {.*} # Flush buffer return $context_mode } # ---------------------------------------------------------------------------- # get_contexts # ---------------------------------------------------------------------------- # Show contexts, parse output, append context names into list contexts. # Returns list of context names, starting with system. # proc get_contexts {} { global prompt global spawn_id set contexts [list system] send -- "show context\r" expect { default { abort "Unknown condition occured at 'show context'" } timeout { abort "Timeout condition occured at 'show context'" } -re $prompt {} } foreach line [split $expect_out(buffer) "\n"] { send_user -- "Line: '$line'" if {[regexp {^[ \*]([A-Za-z0-9\-]+)} $line match0 match1]} { lappend contexts $match1 } } expect -re {.*} # Flush buffer return $contexts } # ---------------------------------------------------------------------------- # get_interface_hack # ---------------------------------------------------------------------------- # When accessing ASA through a VPN tunnel and doing a copy command, the ASA # uses the public interface IP as source address. The copy command then fails # because traffic is not encrypted through the VPN tunnel. Appending the # option ";int=inside" is an undocumented hack to use the inside IP address # instead. Works only for the copy command, not for the backup command. # proc get_interface_hack {} { global prompt global spawn_id set interface_hack "" send -- "show interface | include inside\r" expect { default { abort "Unknown condition occured at 'show mode'" } timeout { abort "Timeout at 'show mode'" } -re {Interface.*inside.*is up} {set interface_hack ";int=inside"; exp_continue} -re {ERROR: % Invalid input detected at} {exp_continue} -re $prompt {} } send_user -- "Interface hack: '$interface_hack'\n" expect -re {.*} # Flush buffer return $interface_hack } # ---------------------------------------------------------------------------- # copy_file # ---------------------------------------------------------------------------- # Does what you expect. Copies a file from source to destination. Applies the # inside interface hack for source NAT over VPN. # proc copy_file {source destination} { global prompt global spawn_id global interface_hack expect -re {.*} # Flush buffer send -- "copy /noconfirm ${source} ${destination}${interface_hack}\r" expect { default { abort "Unknown condition occured at copy ${source}." } timeout { abort "Timeout occured at copy ${source}." } -re {!+} {exp_continue} -re {INFO: No digital signature found} {exp_continue} -re {bytes copied in .* secs} {exp_continue} -re {No such file or directory} { abort "No such file or directory" } -re $prompt {} } expect -re {.*} # Flush buffer return } # ---------------------------------------------------------------------------- # delete_file # ---------------------------------------------------------------------------- proc delete_file {filename} { global prompt global spawn_id expect -re {.*} # Flush buffer send -- "delete /noconfirm ${filename}\r" expect { default { abort "Unknown condition occured at delete ${filename}." } timeout { abort "Timeout occured at delete ${filename}." } -re {No such file or directory} { abort "No such file or directory" } -re $prompt {} } expect -re {.*} # Flush buffer return } # ---------------------------------------------------------------------------- # copy_tech_support # ---------------------------------------------------------------------------- # Sending tech-support directly to scp path fails. Copy first to flash disk, # then copy, then delete. # proc copy_tech_support {backup_url suffix } { global prompt global spawn_id expect -re {.*} # Flush buffer send_user -- "Collecting tech-support ...\n" send -- "show tech-support file flash:/tmp_tech-support.txt\r" expect { default { abort "Unknown condition occured at show tech-support" } timeout { abort "Timeout occured at show tech-support" } -re {INFO: No digital signature found} {exp_continue} -re $prompt {} } copy_file "flash:/tmp_tech-support.txt " "${backup_url}/tech-support_${suffix}.txt" delete_file "flash:/tmp_tech-support.txt" expect -re {.*} # Flush buffer return } # ---------------------------------------------------------------------------- # run_backup # ---------------------------------------------------------------------------- # First run a backup to flash disk and then copy it via scp due to Cisco bug # CSCvh02142. Backup of WebVPN data fails in multiple contexts. # proc run_backup {context_mode password backup_url suffix {context ""}} { global prompt global spawn_id expect -re {.*} # Flush buffer if {$context_mode eq "single"} { set backup_cmd "backup /noconfirm passphrase ${password} location flash:/backup.tar.gz\r" set backup_url_file "${backup_url}/backup_${suffix}.tar.gz" } else { set backup_cmd "backup /noconfirm context ${context} passphrase ${password} location flash:/backup.tar.gz\r" set backup_url_file "${backup_url}/backup_${context}_${suffix}.tar.gz" } send -- $backup_cmd expect { default { abort "Unknown condition occured at backup." } timeout { abort "Timeout occured at backup." } -re {Invalid input detected at} {abort "Backup command invalid" } -re {Warning: This device is part of a failover set up.} {exp_continue} -re {Begin backup} {exp_continue} -re {Backing up.*Done} {exp_continue} -re {Compressing the backup directory.*Done} {exp_continue} -re {Copying Backup.*Done} {exp_continue} -re {Cleaning up.*Done} {exp_continue} -re {Backup finished} {exp_continue} -re $prompt {} } copy_file "flash:/backup.tar.gz" "${backup_url_file}" delete_file "flash:/backup.tar.gz" expect -re {.*} # Flush buffer return } # ---------------------------------------------------------------------------- # Main # ---------------------------------------------------------------------------- if {$argc != 2} { abort "Usage: ${argv0} <hostname> <destdir>" } set asa_host [sanitize_hostname [lindex $argv 0]] set dest_dir [sanitize_dir [lindex $argv 1]] set password [read_password $PASSWORD_FILE] set backup_url "scp://${BACKUP_USER}:${password}@${BACKUP_HOST}/${dest_dir}" set prompt {[a-z0-9\-/]*[>\#] $} set suffix [get_retention_suffix] # Create destination directory if it not exists. if {[file exist $dest_dir] && ! [file isdirectory $dest_dir]} { abort "Destination ${dest_dir} exists, but it is not a directory!" } else { file mkdir $dest_dir } send_user -- "\n\n" send_user -- "------------------------------------------------------------\n" send_user -- "Cisco ASA to back up : ${asa_host}\n" send_user -- "Destination directory: ${dest_dir}\n" send_user -- "------------------------------------------------------------\n" connect_firewall $ASA_USER $asa_host $ASA_ENABLE $password set context_mode [get_context_mode] set interface_hack [get_interface_hack] set version [get_version] copy_tech_support $backup_url $suffix if {$version >= 9.003} { # The backup command was added with ASA version 9.3(2). # It contains the running-config, startup-config, certificates and if there # was no bug with multiple contexts, also webvpn data such as anyconnect # packages. if {$context_mode eq "single"} { send_user -- "Single context: Backing up ...\n" run_backup $context_mode $password $backup_url $suffix } else { send_user -- "Multiple contexts: Getting context names ...\n" set contexts [get_contexts] # Iterate through list of contexts and do backup to scp destination. foreach context $contexts { send_user -- "Backing up context $context ...\n" run_backup $context_mode $password $backup_url $suffix $context } } } else { # Legacy ASA version. No backup command. Copy running and startup config. # No export of keys and certificates in pkcs#12 format. copy_file "running-config" "${backup_url}/running-config_${suffix}.cfg" copy_file "startup-config" "${backup_url}/startup-config_${suffix}.cfg" } send -- "exit\r" send_user -- "Finished backup.\n"