preparar_nodo.sh

#!/bin/bash
# Prepara un nodo para el portal de actualizaciones. Se lanza UNA VEZ en cada nodo,
# con sudo, desde la carpeta que contiene también `nodo-mantenimiento`:
#
#   sudo bash preparar_nodo.sh            instala o actualiza (se puede repetir)
#   sudo bash preparar_nodo.sh --quitar   lo deshace todo
#
# Qué hace:
#   1. Crea el usuario de sistema `ansible`, sin contraseña (bloqueada).
#   2. Le autoriza SOLO la clave del portal, solo desde el master-1 (192.0.2.112) y sin
#      reenvíos de puertos, agente ni X11.
#   3. Instala /usr/local/sbin/nodo-mantenimiento (root:root 0755).
#   4. sudo SOLO para `nodo-mantenimiento revisar|actualizar|tailscale|reiniciar|k3s`,
#      sin contraseña. Nada más: no tiene root libre.
#   5. Temporizador que revisa las actualizaciones cada 6 h y al arrancar, para que
#      el panel de Grafana esté al día aunque no se use el portal.
set -euo pipefail
export PATH=/usr/sbin:/usr/bin:/sbin:/bin

USUARIO=ansible
ORIGEN="192.0.2.112"
CLAVE="ssh-ed25519 <CLAVE_PUBLICA> mantenimiento@master-1"
SCRIPT=/usr/local/sbin/nodo-mantenimiento
SUDOERS=/etc/sudoers.d/nodo-mantenimiento
UNIDAD=/etc/systemd/system/nodo-mantenimiento-revisar

[ "$(id -u)" -eq 0 ] || { echo "Lánzalo con sudo: sudo bash $0" >&2; exit 1; }
cd "$(dirname "$0")"

if [ "${1:-}" = "--quitar" ]; then
  systemctl disable --now nodo-mantenimiento-revisar.timer 2>/dev/null || true
  rm -f "$UNIDAD.timer" "$UNIDAD.service" "$SUDOERS" "$SCRIPT"
  rm -f /var/lib/node_exporter/textfile/actualizaciones.prom
  systemctl daemon-reload
  id "$USUARIO" >/dev/null 2>&1 && userdel -r "$USUARIO" 2>/dev/null || true
  echo "✓ Quitado de $(hostname)"
  exit 0
fi

[ -f nodo-mantenimiento ] || { echo "No encuentro nodo-mantenimiento junto a este script" >&2; exit 1; }
command -v python3 >/dev/null || { echo "Falta python3 (Ansible lo necesita): apt install python3" >&2; exit 1; }

echo "1/5 Usuario $USUARIO"
if ! id "$USUARIO" >/dev/null 2>&1; then
  useradd --system --create-home --home-dir /var/lib/$USUARIO --shell /bin/bash "$USUARIO"
fi
passwd -l "$USUARIO" >/dev/null

echo "2/5 Clave SSH del portal (solo desde $ORIGEN)"
HOME_A=$(getent passwd "$USUARIO" | cut -d: -f6)
install -d -m 700 -o "$USUARIO" -g "$USUARIO" "$HOME_A/.ssh"
printf 'from="%s",no-agent-forwarding,no-port-forwarding,no-X11-forwarding %s\n' "$ORIGEN" "$CLAVE" \
  > "$HOME_A/.ssh/authorized_keys"
chown "$USUARIO:$USUARIO" "$HOME_A/.ssh/authorized_keys"
chmod 600 "$HOME_A/.ssh/authorized_keys"

echo "3/5 $SCRIPT"
install -o root -g root -m 755 nodo-mantenimiento "$SCRIPT"

echo "4/5 sudo limitado"
TMP=$(mktemp)
cat > "$TMP" <<EOF
# Portal de actualizaciones: el usuario $USUARIO solo puede lanzar estas órdenes exactas.
# Sin líneas Defaults: Ubuntu 26.04 usa sudo-rs, que no conoce opciones como requiretty.
$USUARIO ALL=(root) NOPASSWD: $SCRIPT revisar, $SCRIPT actualizar, $SCRIPT tailscale, $SCRIPT reiniciar, $SCRIPT k3s
EOF
visudo -cf "$TMP" >/dev/null || { echo "sudoers no válido: no instalo nada más" >&2; rm -f "$TMP"; exit 1; }
install -o root -g root -m 440 "$TMP" "$SUDOERS"
rm -f "$TMP"

echo "5/5 Revisión automática cada 6 h"
cat > "$UNIDAD.service" <<EOF
[Unit]
Description=Revisa las actualizaciones pendientes (métricas para Grafana)
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=$SCRIPT revisar
Nice=10
IOSchedulingClass=idle
EOF
cat > "$UNIDAD.timer" <<EOF
[Unit]
Description=Revisa las actualizaciones pendientes cada 6 h

[Timer]
OnBootSec=5min
OnUnitActiveSec=6h
RandomizedDelaySec=10min
Persistent=true

[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now nodo-mantenimiento-revisar.timer >/dev/null
systemctl start nodo-mantenimiento-revisar.service

echo
echo "✓ $(hostname) listo:"
sudo -n -u "$USUARIO" sudo -n -l "$SCRIPT" revisar >/dev/null && echo "  · sudo limitado OK"
sudo -n -u "$USUARIO" sudo -n true 2>/dev/null && echo "  ✘ ¡$USUARIO tiene sudo libre! Revisa /etc/sudoers.d" || echo "  · sin sudo libre OK"
[ -s /var/lib/node_exporter/textfile/actualizaciones.prom ] && echo "  · métricas escritas OK"