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 지연 없어야 함)"