centos7_web_stack_hardening_v3.sh

centos7_web_stack_hardening_v3.sh
#!/usr/bin/env bash
# centos7_web_stack_hardening_v3.sh
# - Repo pin -> fastestmirror off -> SELinux off
# - limits/sysctl/httpd
# - Miniconda install + ToS non-interactive accept + py312 env
# - SSH 로그인 지연(UseDNS/GSSAPI/DNSv6) 해결
set -euo pipefail

TS="$(date +%Y%m%d%H%M%S)"
RELEASEVER="7"
BASEARCH="$(uname -m)"

# ===== Conda config =====
INSTALL_MINICONDA=1
MINICONDA_PREFIX="/opt/miniconda"
CONDA_ENV_NAME="py312"
CONDA_PY_VERSION="3.12"
MINICONDA_URL="https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh"

backup_if_exists(){ local f="$1"; [[ -f "$f" ]] && cp -a "$f" "${f}.bak-${TS}" || true; }
ensure_dir(){ local d="$1"; [[ -d "$d" ]] || mkdir -p "$d"; }
set_kv_in_file(){
  # set_kv_in_file <file> <key> <value>
  local f="$1" k="$2" v="$3"
  backup_if_exists "$f"
  if grep -qE "^[#[:space:]]*${k}[[:space:]]" "$f"; then
    sed -ri "s|^[#[:space:]]*(${k})[[:space:]].*|\\1 ${v}|g" "$f"
  else
    echo "${k} ${v}" >> "$f"
  fi
}

echo "[1/8] Pin CentOS-Base.repo to Kakao + disable mirrorlist/fastestmirror"
YUM_REPO="/etc/yum.repos.d/CentOS-Base.repo"
backup_if_exists "$YUM_REPO"
sed -ri 's/^[[:space:]]*mirrorlist=/#mirrorlist=/g' "$YUM_REPO" || true

declare -A BASEURLS=(
  ["base"]="http://mirror.kakao.com/centos/${RELEASEVER}/os/${BASEARCH}/"
  ["updates"]="http://mirror.kakao.com/centos/${RELEASEVER}/updates/${BASEARCH}/"
  ["extras"]="http://mirror.kakao.com/centos/${RELEASEVER}/extras/${BASEARCH}/"
  ["centosplus"]="http://mirror.kakao.com/centos/${RELEASEVER}/centosplus/${BASEARCH}/"
)

for section in "${!BASEURLS[@]}"; do
  if grep -q "^\[$section\]" "$YUM_REPO"; then
    awk -v sec="$section" -v url="${BASEURLS[$section]}" '
      BEGIN{insec=0; rep=0}
      /^\[/{insec=0}
      $0=="["sec"]"{insec=1}
      {
        if(insec && $0 ~ /^[[:space:]]*baseurl=/ && rep==0){ print "baseurl="url; rep=1; next }
        print
      }' "$YUM_REPO" > "${YUM_REPO}.tmp" && mv "${YUM_REPO}.tmp" "$YUM_REPO"
    if ! awk -v sec="$section" '
      BEGIN{f=0;insec=0}
      /^\[/{insec=0}
      $0=="["sec"]"{insec=1}
      { if(insec && $0 ~ /^[[:space:]]*baseurl=/) f=1 }
      END{exit(f?0:1)}
    ' "$YUM_REPO"; then
      awk -v sec="$section" -v url="${BASEURLS[$section]}" '
        { print; if($0=="["sec"]") print "baseurl="url }' "$YUM_REPO" > "${YUM_REPO}.tmp" && mv "${YUM_REPO}.tmp" "$YUM_REPO"
    fi
  else
    cat >> "$YUM_REPO" <<EOF

[$section]
name=CentOS-\$releasever - ${section^}
#mirrorlist=http://mirrorlist.centos.org/?release=\$releasever&arch=\$basearch&repo=$section&infra=\$infra
baseurl=${BASEURLS[$section]}
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
$( [[ "$section" == "centosplus" ]] && echo "enabled=0" )
EOF
  fi
done

ensure_dir "/etc/yum/pluginconf.d"
FM_CONF="/etc/yum/pluginconf.d/fastestmirror.conf"
if [[ -f "$FM_CONF" ]]; then
  backup_if_exists "$FM_CONF"
  sed -ri 's/^[[:space:]]*enabled=.*/enabled=0/g' "$FM_CONF"
else
  echo -e "[main]\nenabled=0" > "$FM_CONF"
fi

yum -y install yum-utils curl ca-certificates || true
yum clean all || true
yum makecache fast || true

echo "[2/8] Disable SELinux (runtime + persistent)"
command -v setenforce >/dev/null 2>&1 && setenforce 0 || true
backup_if_exists "/etc/selinux/config"
sed -ri 's/^SELINUX=enforcing/SELINUX=disabled/g; s/^SELINUX=permissive/SELINUX=disabled/g' /etc/selinux/config

echo "[3/8] limits.conf via /etc/security/limits.d/99-web.conf"
LIMITS_D="/etc/security/limits.d"
ensure_dir "$LIMITS_D"
cat > "$LIMITS_D/99-web.conf" <<'EOF'
*       soft    nofile  65535
*       hard    nofile  65535
*       soft    nproc   65535
*       hard    nproc   65535
apache  soft    nofile  65535
apache  hard    nofile  65535
EOF

echo "[4/8] systemd drop-in for httpd limits"
HTTPD_DROPIN="/etc/systemd/system/httpd.service.d"
ensure_dir "$HTTPD_DROPIN"
cat > "$HTTPD_DROPIN/override.conf" <<'EOF'
[Service]
LimitNOFILE=65535
LimitNPROC=65535
EOF

echo "[5/8] sysctl.d/99-web.conf"
SYSCTL_F="/etc/sysctl.d/99-web.conf"
cat > "$SYSCTL_F" <<'EOF'
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
fs.file-max = 2097152
fs.inotify.max_user_instances = 8192
fs.inotify.max_user_watches   = 1048576
EOF
sysctl --system >/dev/null

echo "[6/8] httpd install/restart (idempotent)"
rpm -q httpd >/dev/null 2>&1 || yum -y install httpd || true
systemctl daemon-reload
systemctl daemon-reexec || true
systemctl enable httpd || true
systemctl restart httpd || true

if [[ "$INSTALL_MINICONDA" -eq 1 ]]; then
  echo "[7/8] Miniconda install + ToS accept + ${CONDA_ENV_NAME} (Python ${CONDA_PY_VERSION})"
  TMP_SH="/tmp/miniconda.sh"
  curl -fsSL "$MINICONDA_URL" -o "$TMP_SH"

  if [[ ! -x "${MINICONDA_PREFIX}/bin/conda" ]]; then
    bash "$TMP_SH" -b -p "$MINICONDA_PREFIX"
  fi
  chmod -R a+rx "$MINICONDA_PREFIX" || true

  # PATH export (system-wide, non-interactive)
  CONDA_PROFILE="/etc/profile.d/miniconda.sh"
  echo 'if [ -d "/opt/miniconda/bin" ]; then export PATH="/opt/miniconda/bin:$PATH"; fi' > "$CONDA_PROFILE"
  chmod 644 "$CONDA_PROFILE"
  ln -sf "${MINICONDA_PREFIX}/bin/conda" /usr/local/bin/conda

  # ---- ToS non-interactive accept (system-wide/root) ----
  # 필요 채널: main, r
  "${MINICONDA_PREFIX}/bin/conda" tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main || true
  "${MINICONDA_PREFIX}/bin/conda" tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r || true

  # (선택) 기본 채널을 시스템 레벨에 명시
  "${MINICONDA_PREFIX}/bin/conda" config --system --remove-key default_channels >/dev/null 2>&1 || true
  "${MINICONDA_PREFIX}/bin/conda" config --system --add default_channels https://repo.anaconda.com/pkgs/main
  "${MINICONDA_PREFIX}/bin/conda" config --system --add default_channels https://repo.anaconda.com/pkgs/r

  # env 생성 (비대화식)
  if [[ ! -d "${MINICONDA_PREFIX}/envs/${CONDA_ENV_NAME}" ]]; then
    "${MINICONDA_PREFIX}/bin/conda" create -y -n "${CONDA_ENV_NAME}" "python=${CONDA_PY_VERSION}"
  fi
  "${MINICONDA_PREFIX}/bin/conda" run -n "${CONDA_ENV_NAME}" python -V || true
fi

echo "[8/8] SSH 로그인 지연 튜닝 (UseDNS/GSSAPI/IPv6 회피)"
SSHD="/etc/ssh/sshd_config"
backup_if_exists "$SSHD"
# UseDNS no, GSSAPIAuthentication no, AddressFamily inet 설정(존재하면 교체, 없으면 추가)
set_kv_in_file "$SSHD" "UseDNS" "no"
set_kv_in_file "$SSHD" "GSSAPIAuthentication" "no"
set_kv_in_file "$SSHD" "AddressFamily" "inet"
# (선택) LoginGraceTime 단축
set_kv_in_file "$SSHD" "LoginGraceTime" "20"

# hostname 역조회가 실패해 지연되는 것을 방지하려면 /etc/hosts에 서버 호스트네임을 루프백/고정IP로 매핑 권장
HN="$(hostname -s)"
if ! grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[[:space:]]+${HN}( |$)" /etc/hosts; then
  echo -e "127.0.0.1\t${HN}" >> /etc/hosts
fi

systemctl restart sshd

echo "== DONE =="
echo "[verify]"
echo "  - conda -V ; ${MINICONDA_PREFIX}/bin/conda info"
echo "  - ${MINICONDA_PREFIX}/bin/conda env list | grep ${CONDA_ENV_NAME} || true"
echo "  - grep -nE 'UseDNS|GSSAPIAuthentication|AddressFamily' /etc/ssh/sshd_config"
echo "  - ssh -vvv localhost (GSSAPI/DNS lookup 지연 없어야 함)"