# 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: ```bash 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 ```bash # 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`](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.