prole/infrastructure/roles/tplink_switch_harden/templates/harden.exp.j2
chrisfu ca8dd6a525 Add TP-Link SG2428LP switch hardening as repeatable IaC + static IP/DNS
Secure and configure the SG2428LP managed switch (10.0.0.10) as repeatable
Ansible, driven from the control node over SSH by an expect engine.

- tplink_switch_harden role (verified JetStream CLI syntax): HTTP off/
  HTTPS-only, Telnet off, SSH v2, SNMP off, RSTP + loopback-detection,
  then copy running-config startup-config (persist).
- playbooks/harden_switch.yml + [switches] inventory group; dry by default,
  -e switch_apply=true to apply, -e switch_apply_network=true for addressing.
- Static mgmt IP 10.0.0.10/24 (off DHCP, below the .20-.199 pool), gw 10.0.0.1,
  hostname sg2428lp.
- op -> ansible-vault bridge (etc/set-switch-1password.sh); admin password in
  group_vars/switches/vault.yml.
- DNS: A/PTR sg2428lp.prole.org -> 10.0.0.10 (records added to dns.yml/ptr_records).
- Fix: tag the samba_reverse_dns zone-list set_fact so tag-limited runs define
  samba_reverse_zones.

Engine notes (hard-won): forces password-only SSH auth (the switch drops
publickey-first logins); always saves config (unsaved changes revert on reboot).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 00:20:55 -07:00

93 lines
3.2 KiB
Django/Jinja

#!/usr/bin/expect -f
# RENDERED by Ansible (role: tplink_switch_harden). Do not edit the rendered copy.
# Drives a TP-Link JetStream switch CLI over SSH. Password comes from $env(SWITCH_PW)
# so it never appears in this file, the inventory, or the process arguments.
set timeout {{ switch_expect_timeout | default(30) }}
set host "{{ switch_mgmt_ip }}"
set user "{{ switch_admin_user }}"
set pw $env(SWITCH_PW)
set apply {{ '1' if switch_apply | bool else '0' }}
log_user 1
# Device prompt: User EXEC '>' or Privileged '#', optionally prefixed by hostname.
set PROMPT {[\w./-]*[>#] ?$}
proc wait_prompt {} {
global PROMPT
expect {
-re {(?i)--more--|press any key|\(q to quit\)} { send -- " "; exp_continue }
-re $PROMPT {}
timeout { puts "\n>>>TIMEOUT-AT-PROMPT<<<"; exit 21 }
eof { puts "\n>>>EOF-AT-PROMPT<<<"; exit 22 }
}
}
proc do {cmd} {
send -- "$cmd\r"
wait_prompt
}
# NOTE: PubkeyAuthentication=no + PreferredAuthentications=password is REQUIRED.
# The JetStream SSH server drops the connection when the client offers publickey
# auth first (the default), which made logins appear to "close after KEX" at
# random. Forcing password-only auth makes login deterministic.
spawn ssh -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=15 -o NumberOfPasswordPrompts=1 \
-o PreferredAuthentications=password -o PubkeyAuthentication=no $user@$host
expect {
-re {(?i)password:} { log_user 0; send -- "$pw\r"; log_user 1; exp_continue }
-re {(?i)(change.*password|new password|must.*change|set.*new password)} {
puts "\n>>>FORCED-PASSWORD-CHANGE<<< switch still at first-login; aborting."; exit 9
}
-re {(?i)(permission denied|authentication fail)} { puts "\n>>>AUTH-FAILED<<<"; exit 8 }
-re $PROMPT {}
timeout { puts "\n>>>LOGIN-TIMEOUT<<<"; exit 2 }
eof { puts "\n>>>LOGIN-EOF<<<"; exit 3 }
}
# Enter privileged EXEC (default enable password is blank).
send -- "enable\r"
expect {
-re {(?i)password:} { send -- "\r"; wait_prompt }
-re $PROMPT {}
timeout {}
}
# Always capture a read-only snapshot — proves access, changes nothing.
puts "\n>>>===SHOW-START===<<<"
do "show system-info"
puts "\n>>>===SHOW-END===<<<"
if {$apply == 1} {
puts "\n>>>===APPLY-START===<<<"
do "configure"
{% for c in switch_harden_commands %}
do {{ "{" ~ c.cmd ~ "}" }} ;# {{ c.desc }}
{% endfor %}
{% for c in switch_identity_commands %}
do {{ "{" ~ c.cmd ~ "}" }} ;# {{ c.desc }}
{% endfor %}
{% if switch_apply_network | bool %}
# NETWORK re-addressing. In steady state (already at the static IP) these are
# no-ops. During the one-time DHCP->static cutover the `ip address` line drops
# the session — do that via the dedicated migration step, not this engine.
{% for c in switch_network_commands %}
do {{ "{" ~ c.cmd ~ "}" }} ;# {{ c.desc }}
{% endfor %}
{% endif %}
do "end"
# Persist running-config to startup (JetStream may ask Y/N).
send -- "copy running-config startup-config\r"
expect {
-re {(?i)(y/n|are you sure|\[y/n\]|continue)} { send -- "Y\r"; wait_prompt }
-re $PROMPT {}
timeout {}
}
puts "\n>>>===APPLY-END===<<<"
}
send -- "exit\r"
catch {expect eof}
puts "\n>>>===DONE===<<<"