#!/bin/bash
set -euo pipefail

###############################################################################
# Rocky Linux 운영 서버 OS Bootstrap 스크립트
#
# 목적:
# - Node.js / Next.js / Laravel / MySQL 등 어떤 서비스를 올리기 전,
#   공통적으로 적용해야 하는 OS 기본 보안/운영 기준을 한 번에 수행하기 위함.
# - 본 스크립트는 docs/00-os 에 정의된 정책을 실제 서버에 적용하는
#   "최초 공통 구축 단계"를 자동화한 기준 레퍼런스임.
#
# 전제:
# - Rocky Linux 9 계열
# - 초기 1회는 root 또는 sudo 권한이 있는 계정으로 실행
# - SELinux Enforcing 유지가 기본 기준 (Disable/Permissive 금지)
#
# 포함 범위:
# - 시스템 업데이트 및 기본 유틸/운영 패키지 설치
# - dnf-automatic 기반 보안 업데이트 자동 적용
# - 운영자 계정 생성 및 sudo 권한 부여
# - SSH 기본 Hardening (root 로그인 차단, 인증 방식 제한 등)
# - firewalld 기본 정책 적용 (SSH 포트/소스 제한 반영)
# - Fail2ban 설치 및 SSH 보호 기본값
# - journald 로그 용량 제한 정책 적용
# - cron.allow, sysctl, core dump 제한 등 기본 Hardening
#
# 제외 범위:
# - Nginx, Node.js, Next.js, PHP, MySQL 등 개별 서비스 설치/구성
# - /tmp, /var/tmp, /home 등 mount/fstab 구조 설계(시스템 구조에 따라 개별 결정)
###############################################################################

BACKUP_SUFFIX="$(date +%Y%m%d_%H%M%S)"

backup_file() {
  local target="$1"
  if [ -f "${target}" ]; then
    cp "${target}" "${target}.bak.${BACKUP_SUFFIX}"
  fi
}

remove_rich_rules_containing() {
  local needle="$1"
  local rule

  while IFS= read -r rule; do
    [ -z "${rule}" ] && continue
    if printf '%s\n' "${rule}" | grep -Fq "${needle}"; then
      firewall-cmd --permanent --remove-rich-rule="${rule}" >/dev/null 2>&1 || true
    fi
  done < <(firewall-cmd --permanent --list-rich-rules 2>/dev/null || true)
}

require_root() {
  if [ "${EUID}" -ne 0 ]; then
    echo "❌ 이 스크립트는 root 권한으로 실행해야 합니다."
    echo "   예: sudo bash scripts/os/os-bootstrap.sh"
    exit 1
  fi
}

configure_selinux_ssh_port() {
  if ! command -v getenforce >/dev/null 2>&1; then
    return 0
  fi

  local selinux_mode
  selinux_mode="$(getenforce || true)"

  if [ "${selinux_mode}" != "Enforcing" ] || [ "${SSH_PORT}" = "22" ]; then
    return 0
  fi

  echo "SSH 포트가 22가 아니므로 SELinux ssh_port_t 등록을 적용합니다."

  if ! command -v semanage >/dev/null 2>&1; then
    echo "❌ semanage 명령을 찾지 못했습니다. policycoreutils-python-utils 설치를 확인하십시오."
    exit 1
  fi

  if semanage port -l | grep -E "^ssh_port_t[[:space:]]+tcp[[:space:]]+.*(^|,| )${SSH_PORT}([,-]|$)" >/dev/null 2>&1; then
    echo "SELinux ssh_port_t 에 tcp/${SSH_PORT} 가 이미 등록되어 있습니다."
    return 0
  fi

  semanage port -a -t ssh_port_t -p tcp "${SSH_PORT}" 2>/dev/null || \
    semanage port -m -t ssh_port_t -p tcp "${SSH_PORT}"
}

require_root

ADMIN_USER="${ADMIN_USER:-adminuser}"
ADMIN_GROUP="${ADMIN_GROUP:-}"
SSH_PORT="${SSH_PORT:-22}"

# 선택 옵션
# - PROFILE: internal | public (서버 SSH 노출 형태에 따른 기본 정책 프리셋)
# - SSH_ALLOW_CIDR: SSH를 특정 CIDR에서만 허용하고 싶을 때 사용 (예: 192.168.0.0/24)
# - ADMIN_AUTHORIZED_KEY: 운영자 공개키 1줄(ssh-rsa/ed25519 등)을 넣으면 authorized_keys에 자동 반영
# - FAIL2BAN_IGNOREIP_EXTRA: fail2ban ignoreip에 추가할 IP/CIDR (공백 구분)
PROFILE="${PROFILE:-internal}"
SSH_ALLOW_CIDR="${SSH_ALLOW_CIDR:-}"
ADMIN_AUTHORIZED_KEY="${ADMIN_AUTHORIZED_KEY:-}"
FAIL2BAN_IGNOREIP_EXTRA="${FAIL2BAN_IGNOREIP_EXTRA:-}"

echo "=== [0] 사전 확인 ==="
echo "Rocky Linux 버전:"
cat /etc/os-release || true
echo

echo "SELinux 상태 (Enforcing 유지가 기본 기준):"
getenforce || true
echo

echo "관리용 운영자 계정: ${ADMIN_USER}"
echo "SSH 포트: ${SSH_PORT}"
echo "운영 프로파일: ${PROFILE}"
if [ -n "${SSH_ALLOW_CIDR}" ]; then
  echo "SSH 허용 CIDR: ${SSH_ALLOW_CIDR}"
fi
echo

if [ "${PROFILE}" != "internal" ] && [ "${PROFILE}" != "public" ]; then
  echo "❌ PROFILE 값이 올바르지 않습니다. internal|public 중 하나여야 합니다."
  echo "   현재 값: ${PROFILE}"
  exit 1
fi

if [ "${PROFILE}" = "public" ] && [ -z "${ADMIN_AUTHORIZED_KEY}" ]; then
  echo "❌ PROFILE=public 인데 ADMIN_AUTHORIZED_KEY 가 비어 있습니다."
  echo "   외부 노출 SSH 운영 서버는 공개키 기반 인증만 허용하는 것을 기준으로 합니다."
  echo "   ADMIN_AUTHORIZED_KEY 를 전달하거나, PROFILE=internal 로 진행하십시오."
  exit 1
fi

###############################################################################
echo "=== [1] 시스템 업데이트 및 기본 유틸/운영 패키지 설치 ==="
dnf -y update
dnf -y install epel-release
if ! dnf -y install \
  vim git htop curl wget net-tools firewalld dnf-automatic \
  policycoreutils policycoreutils-python-utils setools-console audit \
  fail2ban fail2ban-firewalld; then
  echo "❌ 시스템 업데이트 또는 기본 패키지 설치에 실패했습니다."
  exit 1
fi
echo "✅ 시스템 업데이트 및 기본 패키지 설치 완료"

###############################################################################
echo "=== [2] 운영자 계정 생성 및 sudo 권한 부여 ==="

if id "${ADMIN_USER}" >/dev/null 2>&1; then
  echo "운영자 계정 ${ADMIN_USER} 가 이미 존재합니다. 생성을 건너뜁니다."
else
  useradd -m -s /bin/bash "${ADMIN_USER}"
  echo "계정 ${ADMIN_USER} 가 생성되었습니다. 비밀번호는 다음 명령으로 설정하십시오:"
  echo "  passwd ${ADMIN_USER}"
fi

ADMIN_GROUP="${ADMIN_GROUP:-$(id -gn "${ADMIN_USER}")}"

echo "wheel 그룹에 운영자 계정을 추가합니다."
usermod -aG wheel "${ADMIN_USER}"

echo "SSH 접속 준비를 위해 .ssh 디렉터리 권한을 정리합니다."
install -d -m 700 -o "${ADMIN_USER}" -g "${ADMIN_GROUP}" "/home/${ADMIN_USER}/.ssh"

if [ -n "${ADMIN_AUTHORIZED_KEY}" ]; then
  echo "ADMIN_AUTHORIZED_KEY 가 제공되어 authorized_keys를 구성합니다."
  AUTH_KEYS="/home/${ADMIN_USER}/.ssh/authorized_keys"
  touch "${AUTH_KEYS}"
  chmod 600 "${AUTH_KEYS}"
  chown "${ADMIN_USER}:${ADMIN_GROUP}" "${AUTH_KEYS}"
  if ! grep -qF "${ADMIN_AUTHORIZED_KEY}" "${AUTH_KEYS}"; then
    echo "${ADMIN_AUTHORIZED_KEY}" >> "${AUTH_KEYS}"
  fi
fi

echo "✅ 운영자 계정 생성 및 권한 부여 완료"

###############################################################################
echo "=== [3] 보안 업데이트 자동 적용 설정 (dnf-automatic) ==="

DNF_AUTOMATIC_CONF="/etc/dnf/automatic.conf"
backup_file "${DNF_AUTOMATIC_CONF}"

cat > "${DNF_AUTOMATIC_CONF}" <<'EOF'
[commands]
upgrade_type = security
random_sleep = 0
download_updates = yes
apply_updates = yes
reboot = no
EOF

systemctl enable --now dnf-automatic.timer
systemctl status dnf-automatic.timer -n 100 --no-pager || true
echo "✅ dnf-automatic 보안 업데이트 자동 적용 설정 완료"

###############################################################################
echo "=== [4] SSH 기본 Hardening 정책 파일 작성 ==="

SSHD_DIR="/etc/ssh/sshd_config.d"
CUSTOM_SSH_CONF="${SSHD_DIR}/00-custom.conf"
PREVIOUS_SSH_PORT=""

mkdir -p "${SSHD_DIR}"
if [ -f "${CUSTOM_SSH_CONF}" ]; then
  PREVIOUS_SSH_PORT="$(awk '/^[[:space:]]*Port[[:space:]]+/ { print $2; exit }' "${CUSTOM_SSH_CONF}")"
fi
backup_file "${CUSTOM_SSH_CONF}"

cat > "${CUSTOM_SSH_CONF}" <<EOF
# Custom SSH policy (baseline)
Port ${SSH_PORT}
PermitRootLogin no
PasswordAuthentication $( [ "${PROFILE}" = "public" ] && echo "no" || echo "yes" )
PubkeyAuthentication yes
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
UsePAM yes

AllowUsers ${ADMIN_USER}

MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowTcpForwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
UseDNS no
EOF

chown root:root "${CUSTOM_SSH_CONF}"
chmod 600 "${CUSTOM_SSH_CONF}"

###############################################################################
echo "=== [5] firewalld 기본 정책 (SSH 포트/소스 반영) ==="

systemctl enable --now firewalld
firewall-cmd --set-default-zone=public

# 불필요 서비스 제거 시도 (실패해도 무시)
firewall-cmd --permanent --remove-service=dhcpv6-client >/dev/null 2>&1 || true
firewall-cmd --permanent --remove-service=cockpit >/dev/null 2>&1 || true
dnf -y remove cockpit >/dev/null 2>&1 || true

firewall-cmd --permanent --remove-service=ssh >/dev/null 2>&1 || true
firewall-cmd --permanent --remove-port="22/tcp" >/dev/null 2>&1 || true
firewall-cmd --permanent --remove-port="${SSH_PORT}/tcp" >/dev/null 2>&1 || true
if [ -n "${PREVIOUS_SSH_PORT}" ] && [ "${PREVIOUS_SSH_PORT}" != "${SSH_PORT}" ]; then
  firewall-cmd --permanent --remove-port="${PREVIOUS_SSH_PORT}/tcp" >/dev/null 2>&1 || true
fi

remove_rich_rules_containing 'service name="ssh"'
remove_rich_rules_containing "port port=\"${SSH_PORT}\" protocol=\"tcp\" accept"
if [ -n "${PREVIOUS_SSH_PORT}" ] && [ "${PREVIOUS_SSH_PORT}" != "${SSH_PORT}" ]; then
  remove_rich_rules_containing "port port=\"${PREVIOUS_SSH_PORT}\" protocol=\"tcp\" accept"
fi

if [ -n "${SSH_ALLOW_CIDR}" ]; then
  SSH_RICH_RULE="rule family=\"ipv4\" source address=\"${SSH_ALLOW_CIDR}\" port port=\"${SSH_PORT}\" protocol=\"tcp\" accept"
  echo "SSH를 ${SSH_ALLOW_CIDR} 에서만 허용하도록 설정합니다."
  if ! firewall-cmd --permanent --query-rich-rule="${SSH_RICH_RULE}" >/dev/null 2>&1; then
    firewall-cmd --permanent --add-rich-rule="${SSH_RICH_RULE}"
  fi
else
  echo "SSH를 전체 소스에 대해 허용합니다."
  if [ "${SSH_PORT}" = "22" ]; then
    if ! firewall-cmd --permanent --query-service=ssh >/dev/null 2>&1; then
      firewall-cmd --permanent --add-service=ssh
    fi
  else
    if ! firewall-cmd --permanent --query-port="${SSH_PORT}/tcp" >/dev/null 2>&1; then
      firewall-cmd --permanent --add-port="${SSH_PORT}/tcp"
    fi
  fi
fi

echo "불필요한 forward 기능은 비활성화합니다."
firewall-cmd --permanent --zone=public --remove-forward >/dev/null 2>&1 || true
firewall-cmd --reload

echo "현재 firewalld 상태:"
firewall-cmd --list-all
echo "✅ firewalld 기본 정책 설정 완료"

###############################################################################
echo "=== [6] SELinux 포트/도구 기본 준비 (필요 시) ==="
configure_selinux_ssh_port
echo "✅ SELinux 포트 정책 적용 완료"

###############################################################################
echo "=== [7] SSH 설정 검증 및 재시작 ==="
echo "sshd 설정 문법을 검증합니다."
sshd -t

systemctl restart sshd
systemctl status sshd -n 100 --no-pager
echo "✅ SSH 설정 적용 완료"

###############################################################################
echo "=== [8] Fail2ban 설치 및 SSH 보호 기본값 ==="

systemctl enable --now fail2ban

JAIL_LOCAL="/etc/fail2ban/jail.local"
backup_file "${JAIL_LOCAL}"

IGNOREIP="127.0.0.1/8 ::1"
if [ -n "${SSH_ALLOW_CIDR}" ]; then
  IGNOREIP="${IGNOREIP} ${SSH_ALLOW_CIDR}"
fi
if [ -n "${FAIL2BAN_IGNOREIP_EXTRA}" ]; then
  IGNOREIP="${IGNOREIP} ${FAIL2BAN_IGNOREIP_EXTRA}"
fi

F2B_BANTIME="10m"
F2B_FINDTIME="10m"
F2B_MAXRETRY="10"
if [ "${PROFILE}" = "public" ]; then
  # 외부 노출 서버는 조금 더 공격적으로 시작 (필요 시 운영에서 조정)
  F2B_BANTIME="1h"
  F2B_FINDTIME="10m"
  F2B_MAXRETRY="5"
fi

cat > "${JAIL_LOCAL}" <<EOF
[DEFAULT]
ignoreip = ${IGNOREIP}
bantime  = ${F2B_BANTIME}
findtime = ${F2B_FINDTIME}
maxretry = ${F2B_MAXRETRY}
backend  = systemd
banaction = firewallcmd-rich-rules

[sshd]
enabled = true
port    = ${SSH_PORT}
backend = systemd
EOF

systemctl restart fail2ban

fail2ban-client status sshd || true

echo "✅ Fail2ban 설정 완료"

###############################################################################
echo "=== [9] journald 로그 용량 제한 정책 적용 ==="

mkdir -p /etc/systemd/journald.conf.d
JOURNALD_DROPIN="/etc/systemd/journald.conf.d/00-limit.conf"
backup_file "${JOURNALD_DROPIN}"

cat > "${JOURNALD_DROPIN}" <<'EOF'
[Journal]
SystemMaxUse=1G
SystemKeepFree=2G
RuntimeMaxUse=500M
MaxFileSec=1month
Compress=yes
EOF

systemctl restart systemd-journald
journalctl --disk-usage || true
echo "✅ journald 정책 적용 완료"

###############################################################################
echo "=== [10] 기본 System Hardening 정책 적용 ==="

printf "root\n%s\n" "${ADMIN_USER}" > /etc/cron.allow
chmod 600 /etc/cron.allow
chown root:root /etc/cron.allow

SYSCTL_CONF="/etc/sysctl.d/99-security.conf"
backup_file "${SYSCTL_CONF}"
cat > "${SYSCTL_CONF}" <<'EOF'
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.yama.ptrace_scope = 1

net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_syncookies = 1

fs.suid_dumpable = 0

net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0

net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0

net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0

net.core.somaxconn = 8192
net.ipv4.tcp_max_syn_backlog = 8192

net.ipv4.icmp_echo_ignore_broadcasts = 1
EOF

sysctl --system

LIMITS_CONF="/etc/security/limits.conf"
backup_file "${LIMITS_CONF}"
if ! grep -qF "* hard core 0" "${LIMITS_CONF}"; then
  printf '\n* hard core 0\n' >> "${LIMITS_CONF}"
fi

echo "✅ 기본 System Hardening 정책 적용 완료"

###############################################################################
echo "=== [완료] OS 공통 Bootstrap 자동화 단계가 완료되었습니다. ==="
echo "✅ 모든 작업이 성공적으로 완료되었습니다."
echo "- 이제부터는 운영자 계정(${ADMIN_USER})을 통해 SSH 접속 후 작업하는 것을 기준으로 합니다."
echo "- SSH 포트(${SSH_PORT}) 변경 시 firewalld/SELinux 정책까지 함께 반영되도록 처리했습니다."
echo "- /tmp, /var/tmp, /home mount hardening 은 시스템 구조 의존 항목이므로 docs/00-os/09-system-hardening.md 를 따라 별도 적용해야 합니다."
echo "- Nginx, Node.js, Next.js, PHP, MySQL 등 개별 서비스 설치는 docs 하위 각 문서를 따라 단계별로 진행하십시오."
echo
echo "재부팅 후 다음 항목을 확인하는 것을 권장합니다:"
echo "  - systemctl is-active sshd firewalld fail2ban"
echo "  - systemctl is-enabled dnf-automatic.timer"
echo "  - getenforce"
echo "  - firewall-cmd --list-all"
echo "  - journalctl --disk-usage"
echo "  - sysctl kernel.kptr_restrict fs.suid_dumpable"
echo "  - cat /etc/cron.allow"

exit 0

# =============================================================================
# 권한:
# 	sudo chmod 700 ./os-bootstrap.sh
#
# Windows CRLF / UTF-8 BOM 제거가 필요한 경우:
# 	sudo sed -i '1s/^\xEF\xBB\xBF//' ./os-bootstrap.sh
# 	sudo sed -i 's/\r$//' ./os-bootstrap.sh
#
# 문법 확인:
# 	sudo bash -n ./os-bootstrap.sh
#
# 사용법:
#   sudo bash ./os-bootstrap.sh
#   sudo ADMIN_USER=adminuser SSH_PORT=2222 bash ./os-bootstrap.sh
