ssh_solo_claves.sh
#!/usr/bin/env bash
# R10, paso 1: SSH solo con claves en los 6 nodos (PasswordAuthentication no, PermitRootLogin no,
# X11Forwarding no), con el mismo mecanismo que preparar_nodos.sh: un pod privilegiado temporal
# por nodo (DaemonSet) que entra en el nodo con nsenter y después se borra todo.
#
# Uso (como root en el master-1, con el kubeconfig de administrador):
# ./scripts/ssh_solo_claves.sh [CLAVE.pub] solo comprueba, no cambia nada
# ./scripts/ssh_solo_claves.sh --aplicar [CLAVE.pub] quita las contraseñas en los nodos listos
# ./scripts/ssh_solo_claves.sh --deshacer vuelve a dejar el SSH como estaba
#
# CLAVE.pub (por defecto /home/admin/admin-msi.pub) es la clave pública con la que entra admin.
# Un nodo solo se cambia si esa clave está en su authorized_keys de admin Y ya se ha entrado con
# ella en los últimos 2 días (se ve en el registro de sshd): así nadie se queda fuera.
# Si algo falla, --deshacer funciona aunque no se pueda entrar por SSH (va por Kubernetes).
set -euo pipefail
NS=mantenimiento
NOMBRE=ssh-solo-claves
IMAGEN="python:3.13.16-slim-trixie@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f"
MODO=comprobar
case "${1:-}" in
--aplicar) MODO=aplicar; shift ;;
--deshacer) MODO=deshacer; shift ;;
--*|-h) sed -n '2,14p' "$0"; exit 64 ;;
esac
CLAVE="${1:-/home/admin/admin-msi.pub}"
HUELLA=""
if [ "$MODO" != deshacer ]; then
HUELLA=$(ssh-keygen -lf "$CLAVE" | awk '{print $2}')
[ -n "$HUELLA" ] || { echo "No puedo leer la clave $CLAVE"; exit 1; }
fi
limpiar() {
kubectl -n "$NS" delete daemonset "$NOMBRE" --ignore-not-found --wait=false >/dev/null
kubectl -n "$NS" delete configmap "$NOMBRE" --ignore-not-found >/dev/null
}
trap limpiar EXIT
# Lo que se ejecuta dentro de cada nodo.
read -r -d '' NODO <<'EOF' || true
set -u
MODO=$1 HUELLA=$2
F=/etc/ssh/sshd_config.d/00-solo-claves.conf
recargar() { systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null || true; }
mostrar() { sshd -T 2>/dev/null | grep -E '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin|x11forwarding) ' | sed 's/^/ · /'; }
if [ "$MODO" = deshacer ]; then
rm -f "$F"; recargar; mostrar; exit 0
fi
clave=no; usada=no
ssh-keygen -lf /home/admin/.ssh/authorized_keys 2>/dev/null | grep -qF "$HUELLA" && clave=si
journalctl -u ssh -u sshd --since -2d -q 2>/dev/null | grep -F "Accepted publickey for admin" | grep -qF "$HUELLA" && usada=si
echo " · clave en authorized_keys: $clave · ya se ha entrado con ella: $usada"
if [ "$MODO" = comprobar ]; then
mostrar; [ "$clave$usada" = sisi ]; exit $?
fi
# aplicar
[ "$clave$usada" = sisi ] || { echo " · NO se cambia: antes hay que entrar con la clave"; exit 2; }
cat > "$F" <<'CONF'
# R10 paso 1 (scripts/ssh_solo_claves.sh). Va en 00- porque en sshd gana el primer valor.
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
X11Forwarding no
CONF
if ! sshd -t; then rm -f "$F"; echo " · configuración inválida: deshecho"; exit 3; fi
recargar; mostrar
EOF
kubectl get ns "$NS" >/dev/null 2>&1 || kubectl create ns "$NS" >/dev/null
TOTAL=$(kubectl get nodes --no-headers | wc -l)
echo "Modo: $MODO${HUELLA:+ · clave $HUELLA} · $TOTAL nodos"
kubectl -n "$NS" create configmap "$NOMBRE" --from-literal=nodo.sh="$NODO" \
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
kubectl apply -f - >/dev/null <<YAML
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: $NOMBRE
namespace: $NS
spec:
selector:
matchLabels: {app: $NOMBRE}
template:
metadata:
labels: {app: $NOMBRE}
spec:
hostPID: true
tolerations: [{operator: Exists}]
automountServiceAccountToken: false
terminationGracePeriodSeconds: 1
containers:
- name: ssh
image: $IMAGEN
securityContext: {privileged: true}
resources:
requests: {cpu: 10m, memory: 32Mi}
limits: {memory: 128Mi}
command: [bash, -c]
args:
- |
d=/host/var/tmp/ssh-solo-claves
rm -rf "\$d"; mkdir -p "\$d"; cp /ficheros/nodo.sh "\$d/"
nsenter -t 1 -m -u -i -n -p -- bash /var/tmp/ssh-solo-claves/nodo.sh $MODO '$HUELLA'
echo "SSH-FIN rc=\$?"
rm -rf "\$d"
sleep infinity
volumeMounts:
- {name: ficheros, mountPath: /ficheros, readOnly: true}
- {name: host-tmp, mountPath: /host/var/tmp}
volumes:
- {name: ficheros, configMap: {name: $NOMBRE}}
- {name: host-tmp, hostPath: {path: /var/tmp, type: Directory}}
YAML
for _ in $(seq 36); do
hechos=0
for p in $(kubectl -n "$NS" get pods -l app=$NOMBRE -o name 2>/dev/null); do
kubectl -n "$NS" logs "$p" 2>/dev/null | grep -q "SSH-FIN" && hechos=$((hechos + 1))
done
[ "$hechos" -ge "$TOTAL" ] && break
sleep 5
done
echo
listos=0
for p in $(kubectl -n "$NS" get pods -l app=$NOMBRE -o name); do
nodo=$(kubectl -n "$NS" get "$p" -o jsonpath='{.spec.nodeName}')
log=$(kubectl -n "$NS" logs "$p" 2>/dev/null)
rc=$(sed -n 's/^SSH-FIN rc=//p' <<<"$log")
[ "$rc" = 0 ] && { echo "✓ $nodo"; listos=$((listos + 1)); } || echo "✘ $nodo (rc=${rc:-sin terminar})"
grep -E '^\s+· ' <<<"$log" || true
done
echo
echo "$listos de $TOTAL nodos bien."
[ "$listos" -eq "$TOTAL" ]