prole/infrastructure/roles/tplink_switch_harden
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
..
defaults Add TP-Link SG2428LP switch hardening as repeatable IaC + static IP/DNS 2026-06-10 00:20:55 -07:00
tasks Add TP-Link SG2428LP switch hardening as repeatable IaC + static IP/DNS 2026-06-10 00:20:55 -07:00
templates Add TP-Link SG2428LP switch hardening as repeatable IaC + static IP/DNS 2026-06-10 00:20:55 -07:00
README.md Add TP-Link SG2428LP switch hardening as repeatable IaC + static IP/DNS 2026-06-10 00:20:55 -07:00

tplink_switch_harden

Repeatable security hardening for TP-Link JetStream managed switches (developed for the SG2428LP at 10.0.0.153), driven from the control node over SSH by a rendered expect engine.

Secret pipeline

1Password  ──op read──▶  ENV  ──ansible-vault──▶  group_vars/switches/vault.yml  ──▶  playbook
   op://knoey/TP-Link SG2428LP/password         (vault_sg2428lp_admin_password)

Refresh the vault from 1Password whenever the password changes:

etc/set-switch-1password.sh

The admin password is passed to the engine only via the SWITCH_PW environment variable (no_log), so it never lands in inventory, the rendered script, or process arguments.

Usage

# Dry run — log in, capture `show system-info`, change nothing:
ansible-playbook infrastructure/playbooks/harden_switch.yml

# Apply hardening and save to startup-config:
ansible-playbook infrastructure/playbooks/harden_switch.yml -e switch_apply=true

switch_apply defaults to false; nothing is changed until you pass -e switch_apply=true.

What it hardens

Config-mode commands live in defaults/main.yml (switch_harden_commands): disable plaintext HTTP, ensure HTTPS, disable Telnet, SSH v2 only, ensure SNMP off, enable RSTP, enable loopback-detection, then copy running-config startup-config.

The command list was verified against SG2428LP firmware via on-device ? help. JetStream tokens vary by model/firmware — reconfirm with ? if you point this at a different model.

Management IP access-control and CLI idle-timeout are intentionally opt-in (commented in defaults/main.yml) because a wrong ACL can lock you out. Apply those last, from a host inside the permitted range.

Static-IP cutover (DHCP → static)

switch_network_commands (toggled by switch_apply_network) sets the static mgmt IP. In steady state — when the host's ansible_host already equals the static IP — re-asserting it is a no-op. The one-time DHCP→static cutover is different: the ip address line drops the session, so it cannot save afterward over the same connection. Do the cutover deliberately (connect on the DHCP address, set the static IP, reconnect on the new IP, then save), and afterward set the host's ansible_host to the static IP. Same-subnet control hosts reach the new IP directly, so gateway correctness isn't required to regain access.

Gotchas (these cost real debugging time)

  • SSH publickey auth breaks login. The JetStream SSH server drops the connection when the client offers publickey first (the default). The engine forces -o PubkeyAuthentication=no -o PreferredAuthentications=password. Symptom without it: "connection closed after KEX, before password prompt", intermittently (depends on which keys your agent offers).
  • Unsaved config reverts on reboot. Web-UI changes (incl. the first-login password) live in running-config until you explicitly save. A reboot with no save silently reverts to factory. The engine always ends with copy running-config startup-config (look for "Saving user config OK!").
  • One management session at a time. A logged-in web-UI session occupies the single mgmt slot and blocks SSH login. Log out of the web UI before running.
  • no ip http server doesn't close port 80. It disables HTTP management; port 80 stays open serving only a JS redirect to HTTPS. That's expected/secure.