prole/infrastructure/roles/tplink_switch_harden/README.md
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

78 lines
3.4 KiB
Markdown

# 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.