#!/usr/bin/env bash
# harden.sh — fecha o servidor.
#
# Ordem obrigatória, e ela existe por um motivo: cada passo só acontece depois de
# o caminho novo estar provado. Um erro aqui tranca o usuário para fora da própria
# máquina, é a persona do SSHepherd não sabe usar console de recuperação.
#
#   1. exige prova de que o usuário de operação já entra por chave
#   2. libera SSH no firewall ANTES de ligar o firewall
#   3. copia o arquivo de configuração original
#   4. válida a sintaxe antes de aplicar
#   5. recarrega (nunca reinicia) o serviço
#   6. confirma o acesso em conexão nova
#   7. restaura sozinho se a confirmação falhar
#
# Uso: harden.sh --host <ip> --admin <root> --ops <deploy> --validated [--port 22] [--key <chave>] [--app-port 80,443]

set -uo pipefail

HOST=""; ADMIN="root"; OPS="deploy"; PORT=22; KEY_PATH=""; APP_PORTS=""; VALIDATED=0; OPS_KEY=""

while [ $# -gt 0 ]; do
  case "$1" in
    --host)      HOST="$2"; shift 2 ;;
    --admin)     ADMIN="$2"; shift 2 ;;
    --ops)       OPS="$2"; shift 2 ;;
    --port)      PORT="$2"; shift 2 ;;
    --key)       KEY_PATH="$2"; shift 2 ;;
    --ops-key)   OPS_KEY="$2"; shift 2 ;;
    --app-port)  APP_PORTS="$2"; shift 2 ;;
    --validated) VALIDATED=1; shift ;;
    *) echo "argumento desconhecido: $1" >&2; exit 2 ;;
  esac
done

[ -n "$HOST" ] || { echo "ERRO: --host é obrigatório" >&2; exit 2; }

# ---------------------------------------------------------------------------
# PRE-CONDICAO. Sem isto, nada acontece.
# ---------------------------------------------------------------------------
if [ "$VALIDATED" -ne 1 ]; then
  cat >&2 <<'MSG'
BLOQUEADO: pré-condição não atendida.

O endurecimento desliga o acesso de root é o acesso por senha. Se a chave do
usuário de operação não estiver comprovadamente funcionando ANTES disso, o
servidor fica inacessível é a única saída é o console de recuperação do provedor.

Rode primeiro:
  validate-access.sh --host <ip> --user <usuario-de-operacao> --key <chave>

Só passe --validated depois que aquele script tiver retornado access=ok.
MSG
  exit 14
fi

OPS_KEY="${OPS_KEY:-$KEY_PATH}"

echo "Confirmando de novo, por conta própria, que ${OPS} entra por chave..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if ! "${SCRIPT_DIR}/validate-access.sh" --host "$HOST" --user "$OPS" --port "$PORT" ${OPS_KEY:+--key "$OPS_KEY"} | grep -q '^access=ok'; then
  echo "BLOQUEADO: o usuário '${OPS}' não consegue entrar por chave agora." >&2
  echo "Nada foi alterado no servidor." >&2
  exit 14
fi
echo "Confirmado."

SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ForwardAgent=no -o ConnectTimeout=10 -p "$PORT")

# Retrato antes de mexer. O passo 3 já copia o sshd_config, mas o endurecimento
# também instala fail2ban, liga o firewall e mexe no apt: isso e /etc inteiro.
echo "== 0. retrato do servidor antes da mudança =="
"${SCRIPT_DIR}/snapshot-etc.sh" --host "$HOST" --user "$ADMIN" --port "$PORT" \
  ${KEY_PATH:+--key "$KEY_PATH"} --motivo harden || \
  echo "aviso: não consegui gravar o retrato; seguindo assim mesmo" >&2
[ -n "$KEY_PATH" ] && SSH_OPTS+=(-i "${KEY_PATH/#\~/$HOME}" -o IdentitiesOnly=yes)

STAMP=$(date -u +%Y%m%d%H%M%S)

REMOTE=$(cat <<REMOTE_SCRIPT
set -uo pipefail
OPS="${OPS}"
SSH_PORT="${PORT}"
APP_PORTS="${APP_PORTS}"
STAMP="${STAMP}"
export DEBIAN_FRONTEND=noninteractive

echo "== 1. firewall: liberar SSH ANTES de ligar =="
if ! command -v ufw >/dev/null 2>&1; then
  apt-get update -qq >/dev/null 2>&1 || true
  apt-get install -y -qq ufw >/dev/null 2>&1 || { echo "firewall=falha_instalacao"; }
fi
if command -v ufw >/dev/null 2>&1; then
  ufw allow "\${SSH_PORT}/tcp" >/dev/null 2>&1
  if [ -n "\$APP_PORTS" ]; then
    IFS=',' read -ra PORTAS <<< "\$APP_PORTS"
    for p in "\${PORTAS[@]}"; do ufw allow "\${p}/tcp" >/dev/null 2>&1; done
  fi
  # So liga depois que a regra de SSH existe de fato (EC-07 da spec).
  # \`ufw status\` não lista regra com o firewall desligado, e desligado é
  # justamente o caso aqui; \`ufw show added\` lista. A porta vem ancorada para
  # a regra de 2222 não passar por regra de 22.
  if ufw show added | grep -qE "[[:space:]]\${SSH_PORT}/tcp([[:space:]]|\\$)"; then
    ufw default deny incoming >/dev/null 2>&1
    ufw default allow outgoing >/dev/null 2>&1
    yes | ufw enable >/dev/null 2>&1
    echo "firewall=ativo"
  else
    echo "firewall=nao_ativado (regra de SSH ausente, recusa proposital)"
  fi
fi

echo "== 2. copia do arquivo de configuração original =="
BACKUP="/etc/ssh/sshd_config.sshepherd-\${STAMP}.bak"
cp -a /etc/ssh/sshd_config "\$BACKUP"
echo "backup=\$BACKUP"

echo "== 3. escrever a configuração endurecida =="
# Arquivo próprio em conf.d quando a distribuição suporta, senao no arquivo principal.
if grep -qE '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/' /etc/ssh/sshd_config; then
  TARGET="/etc/ssh/sshd_config.d/00-sshepherd.conf"
  mkdir -p /etc/ssh/sshd_config.d
else
  TARGET="/etc/ssh/sshd_config"
  sed -i -E 's/^[[:space:]]*#?[[:space:]]*(PermitRootLogin|PasswordAuthentication|PubkeyAuthentication|ChallengeResponseAuthentication|KbdInteractiveAuthentication|PermitEmptyPasswords|X11Forwarding|MaxAuthTries)[[:space:]].*/# &/' /etc/ssh/sshd_config
fi

cat >> "\$TARGET" <<'SSHCONF'

# ----- SSHepherd -----
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
PermitEmptyPasswords no
X11Forwarding no
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
# ---------------------
SSHCONF
# O verify-day-zero lê este arquivo como usuário de operação; o conteúdo não é segredo.
[ "\$TARGET" = /etc/ssh/sshd_config ] || chmod 644 "\$TARGET"
echo "config_alvo=\$TARGET"

echo "== 4. validar sintaxe ANTES de aplicar =="
if ! sshd -t 2>/tmp/sshp-sshd-test; then
  echo "sintaxe=inválida"
  cat /tmp/sshp-sshd-test
  cp -a "\$BACKUP" /etc/ssh/sshd_config
  rm -f /etc/ssh/sshd_config.d/00-sshepherd.conf 2>/dev/null || true
  echo "restaurado=sim"
  exit 5
fi
echo "sintaxe=válida"

echo "== 5. recarregar o serviço (reload, nunca restart) =="
systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null || service ssh reload 2>/dev/null
echo "reload=feito"

echo "== 6. bloqueio de tentativas repetidas =="
if ! command -v fail2ban-server >/dev/null 2>&1; then
  apt-get install -y -qq fail2ban >/dev/null 2>&1 || true
fi
if command -v fail2ban-server >/dev/null 2>&1; then
  cat > /etc/fail2ban/jail.d/sshp.conf <<F2B
[sshd]
enabled = true
port = \${SSH_PORT}
maxretry = 5
bantime = 3600
findtime = 600
F2B
  systemctl enable --now fail2ban >/dev/null 2>&1
  echo "fail2ban=ativo"
else
  echo "fail2ban=indisponível"
fi

echo "== 7. atualizações automáticas de segurança =="
apt-get install -y -qq unattended-upgrades >/dev/null 2>&1 || true
if [ -d /etc/apt/apt.conf.d ]; then
  cat > /etc/apt/apt.conf.d/20auto-upgrades <<AUTO
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
AUTO
  systemctl enable --now unattended-upgrades >/dev/null 2>&1 || true
  echo "auto_updates=ativo"
fi

echo "backup_path=\$BACKUP"
echo "endurecimento=aplicado"
REMOTE_SCRIPT
)

echo ""
echo "--------------------------------------------"
echo " ENDURECIMENTO de ${HOST}"
echo " Vou desligar: login de root por rede, login por senha"
echo " Vou ligar:    firewall (negando entrada), fail2ban, updates automáticos"
echo " Acesso após:  ${OPS} + chave, apenas"
echo "--------------------------------------------"

OUT=$(ssh "${SSH_OPTS[@]}" "${ADMIN}@${HOST}" "bash -s" <<< "$REMOTE" 2>&1)
CODE=$?
printf '%s\n' "$OUT"

BACKUP_PATH=$(printf '%s' "$OUT" | grep -oE 'backup_path=.*' | head -n1 | cut -d= -f2-)

if [ $CODE -ne 0 ]; then
  echo "" >&2
  echo "O endurecimento falhou (código ${CODE}). O script restaura sozinho a configuração" >&2
  echo "quando a sintaxe e inválida. Copia original em: ${BACKUP_PATH:-/etc/ssh/sshd_config.sshepherd-*.bak}" >&2
  exit $CODE
fi

# ---------------------------------------------------------------------------
# Confirmação em conexão NOVA. Se falhar, restaura pela sessão administrativa
# que ainda esta viva.
# ---------------------------------------------------------------------------
echo ""
echo "== 8. confirmando o acesso em conexão nova =="
sleep 2
if "${SCRIPT_DIR}/validate-access.sh" --host "$HOST" --user "$OPS" --port "$PORT" ${OPS_KEY:+--key "$OPS_KEY"} | grep -q '^access=ok'; then
  echo "acesso_confirmado=sim"
  echo ""
  echo "Servidor endurecido. Rode verify-day-zero.sh para as cinco verificações finais."
  exit 0
fi

echo "acesso_confirmado=NAO — restaurando a configuração anterior" >&2
ssh "${SSH_OPTS[@]}" "${ADMIN}@${HOST}" \
  "cp -a '${BACKUP_PATH}' /etc/ssh/sshd_config && rm -f /etc/ssh/sshd_config.d/00-sshepherd.conf; sshd -t && (systemctl reload ssh || systemctl reload sshd)" 2>&1
echo "restaurado=sim. O acesso anterior continua funcionando." >&2
exit 6
