TL;DR
- RHEL8 모듈 스트림의 httpd는 2.4.37에 영구히 고정되어 있다. 보안 패치는 백포트되지만 버전 표시는 그대로다.
- 2.4.68까지 올리려면 소스 컴파일이 유일한 방법이다.
- 망분리(외부 인터넷 차단) 환경이라면 httpd 소스 tarball만 별도로 반입하면 되고, 빌드 의존성(apr-devel, pcre-devel 등)은 보통 사내 Satellite/구독 미러로 해결된다.
- 운영 서버가 RPM이 아니라 이미 소스 컴파일된 구조라면, dev 서버도 RPM 호환형이 아니라 운영과 동일한 "클래식 레이아웃"(/etc/httpd 한 곳에 bin/conf/modules/logs를 다 모으는 구조)으로 맞춰야 한다.
- 시행착오의 90%는 RHEL이 자체적으로 추가한 비표준 configure 옵션, RPM 제거 시 같이 삭제되는 RPM 소유 디렉토리, 그리고 make install이 기존 conf를 조용히 덮어쓰는 동작에서 나왔다.
0. 배경
운영 중인 RHEL8 서버(httpd 2.4.37, RPM 설치)에 CVE 대응을 위해 Apache httpd 2.4.68로 업그레이드가 필요했다. 다만 같은 서비스를 운영하는 다른 웹서버(ai-oep-web01)는 이미 httpd 2.4.66을 소스 컴파일로 돌리고 있었기 때문에, dev 서버도 운영과 동일한 구조로 맞추는 게 목표였다.
[root@ai-oep-dev ~]# httpd -V
Server version: Apache/2.4.37 (Red Hat Enterprise Linux)
...
-D HTTPD_ROOT="/etc/httpd"
-D SUEXEC_BIN="/usr/sbin/suexec"
...
1. 왜 dnf/yum으로는 안 되는가
RHEL8 AppStream의 httpd 모듈 스트림은 2.4.37에 고정되어 있다. 보안 패치는 들어오지만 버전 넘버 자체는 절대 2.4.68까지 올라가지 않는다. 즉:
RHEL8에서 httpd를 2.4.68로 올리려면 소스 컴파일 외에는 방법이 없다.
2. 1차 시도: RPM 호환형 레이아웃으로 컴파일 (실패는 아니었지만 운영과 안 맞음)
처음엔 RPM이 설치하는 경로 구조(바이너리는 /usr/sbin, /usr/bin, 라이브러리는 /usr/lib64, conf만 /etc/httpd)를 그대로 흉내내려고 아래처럼 configure를 짰다.
./configure \
--prefix=/etc/httpd \
--exec-prefix=/usr \
--bindir=/usr/bin \
--sbindir=/usr/sbin \
--libdir=/usr/lib64 \
--includedir=/usr/include/httpd \
--datadir=/etc/httpd \
--sysconfdir=/etc/httpd/conf \
--localstatedir=/var \
--runtimedir=/run/httpd \
--logfiledir=/var/log/httpd \
--with-apr=/usr/bin/apr-1-config \
--with-apr-util=/usr/bin/apu-1-config \
--with-pcre \
--with-ssl \
--enable-mpms-shared=all \
--with-mpm=event \
--enable-mods-shared=all
삽질 #1 — --runtimedir은 존재하지 않는 옵션
configure: error: unrecognized option: '--runtimedir=/run/httpd'
원인: --runtimedir은 Apache 공식(vanilla) 소스의 configure에는 없는 옵션이다. RHEL이 RPM spec 파일 빌드 시 자체적으로 패치해서 추가한 옵션이었다. 순수 httpd.apache.org에서 받은 소스에는 적용되지 않는다.
→ 해결: 옵션 제거.
삽질 #2 — --logfiledir도 마찬가지
configure: error: unrecognized option: '--logfiledir=/var/log/httpd'
이것도 같은 이유. RHEL 전용 패치 옵션이라 vanilla 소스엔 없다.
→ 해결: 옵션 제거. 로그 디렉토리는 설치 후 심볼릭 링크로 별도 처리하기로 함.
./configure \
--prefix=/etc/httpd \
--exec-prefix=/usr \
--bindir=/usr/bin \
--sbindir=/usr/sbin \
--libdir=/usr/lib64 \
--includedir=/usr/include/httpd \
--datadir=/etc/httpd \
--sysconfdir=/etc/httpd/conf \
--localstatedir=/var \
--with-apr=/usr/bin/apr-1-config \
--with-apr-util=/usr/bin/apu-1-config \
--with-pcre \
--with-ssl \
--enable-mpms-shared=all \
--with-mpm=event \
--enable-mods-shared=all
여기까진 통과했지만, 이후 운영 서버 정보를 확인하면서 이 레이아웃 자체가 운영과 다르다는 걸 깨닫고 전부 다시 잡았다. (3번으로)
교훈: RHEL 계열 httpd RPM의 configure 옵션을 그대로 베껴 쓰면 안 된다. RPM은 spec 파일에서 자체 패치된 옵션을 쓰기 때문에, 공식 소스 tarball의 ./configure --help로 실제 지원 옵션을 먼저 확인해야 한다.
3. 운영 서버 구조부터 먼저 확인했어야 했다
운영 서버(ai-oep-web01)에서 httpd -V를 찍어보니:
Server built: Feb 9 2026 19:29:39
...
-D HTTPD_ROOT="/etc/httpd"
-D SUEXEC_BIN="/etc/httpd/bin/suexec"
-D DEFAULT_PIDLOG="logs/httpd.pid"
-D SERVER_CONFIG_FILE="conf/httpd.conf"
SUEXEC_BIN이 /usr/sbin이 아니라 /etc/httpd/bin이라는 게 핵심 힌트였다. **운영 서버는 모든 산출물(bin, conf, modules, logs)을 /etc/httpd 하나에 다 모아두는 "클래식 Apache 레이아웃"**이었던 것이다. RPM 호환형으로 쪼개서 빌드하면 운영과 구조가 달라진다.
또한 rpm -ql httpd → package httpd is not installed. 운영의 메인 httpd는 RPM이 아니라 순수 소스 빌드였고, httpd-tools/httpd-filesystem만 RPM으로 깔려 있었다.
→ 결론: dev도 --bindir, --sbindir, --libdir 같은 분산 옵션을 다 빼고, --prefix만 줘야 운영과 동일해진다.
./configure \
--prefix=/etc/httpd \
--with-apr=/usr/bin/apr-1-config \
--with-apr-util=/usr/bin/apu-1-config \
--with-pcre \
--with-ssl \
--enable-mpms-shared=all \
--with-mpm=event \
--enable-mods-shared=all
교훈: 운영 서버와 동일하게 맞추는 게 목표라면, 작업을 시작하기 전에 운영 서버의 httpd -V, httpd -M, rpm -ql httpd(또는 미설치 확인), conf 디렉토리 구조를 먼저 전부 뽑아두고 시작했어야 했다. 거꾸로 했더니 빌드를 두 번 했다.
4. 망분리 환경에서 소스 받기
내부망에서 외부 인터넷이 막혀 있는 상황이었다. 다행히 dnf repolist로 확인해보니 사내 RHEL 구독 미러(Satellite)는 살아있어서, 빌드 의존성(gcc, make, pcre-devel, pcre2-devel, openssl-devel, apr-devel, apr-util-devel 등)은 내부망에서 그대로 설치됐다. 외부에서 가져와야 했던 건 httpd 소스 tarball 하나뿐이었다.
4-1. 다운로드 URL에 대한 흔한 오해
https://httpd.apache.org/download.cgi 페이지는 사용자 위치 기반으로 가까운 미러를 안내해주는 페이지일 뿐이고, 실제 파일은 dlcdn.apache.org(CDN, 최신 버전만 보관) 또는 downloads.apache.org(영구 보관)로 연결된다. 어디서 받든 같은 파일이며, 체크섬으로 무결성을 검증할 수 있다.
* Source: httpd-2.4.68.tar.gz [PGP] [SHA256] [SHA512]
여기서 [SHA256] 링크는 검증용 해시값이 담긴 별도의 작은 텍스트 파일이지, httpd 본체가 아니다. tar.gz 본체와 sha256 파일을 둘 다 받아야 한다.
4-2. 삽질 #3 — 파일이 잘려서 반입됨
처음 반입한 tar.gz의 사이즈가 8956KB였는데, 공식 사이즈는 9.6MB(약 9800~9850KB)였다. 사이즈만 봐도 의심스러웠는데, 그냥 진행했다가:
[root@ai-oep-dev parkyi]# tar xzf httpd-2.4.68.tar.gz
gzip: stdin: unexpected end of file
tar: Unexpected EOF in archive
원인: 반입 과정(다운로드 또는 전송)에서 파일이 중간에 끊겼다.
→ 해결: 인터넷 되는 Windows PC에서 PowerShell로 재다운로드 + 체크섬 검증을 거친 뒤 재반입.
Invoke-WebRequest -Uri "https://dlcdn.apache.org/httpd/httpd-2.4.68.tar.gz" -OutFile "httpd-2.4.68.tar.gz"
Invoke-WebRequest -Uri "https://downloads.apache.org/httpd/httpd-2.4.68.tar.gz.sha256" -OutFile "httpd-2.4.68.tar.gz.sha256"
$expected = (Get-Content .\httpd-2.4.68.tar.gz.sha256).Split(" ")[0].Trim()
$actual = (Get-FileHash .\httpd-2.4.68.tar.gz -Algorithm SHA256).Hash
if ($expected -eq $actual) {
Write-Host "OK - 일치합니다" -ForegroundColor Green
} else {
Write-Host "FAILED - 불일치! 재다운로드 필요" -ForegroundColor Red
}
교훈: 사이즈가 의심스러우면 그 즉시 멈추고 체크섬을 확인했어야 했다. "사이즈가 좀 작은 것 같은데 일단 풀어보자"는 시간 낭비로 직결됐다.
5. 빌드 디렉토리 위치
개인 홈 디렉토리(/usr/parkyi 등)에서 빌드해도 결과물 자체는 --prefix로 지정한 경로(/etc/httpd)로 들어가기 때문에 문제는 없다. 다만 관례적으로는 /usr/local/src처럼 공용 위치를 쓰는 것이 좋다. 개인 계정이 삭제되거나 권한이 바뀌면 소스 트리도 같이 사라질 수 있기 때문이다.
주의: --prefix로 지정한 디렉토리(/etc/httpd) 자체를 빌드 작업 디렉토리로 쓰면 안 된다. 거기엔 이미 운영 중인 conf, logs 등이 들어있어서 소스 트리와 뒤섞이면 매우 위험하다.
6. 작업 전 백업 (반드시 먼저 했어야 할 것)
./configure, make까지는 시스템에 전혀 영향을 주지 않는 안전한 작업이다. 하지만 그 이후 rpm -e, make install 단계부터는 운영 중인 서비스에 직접 영향을 준다. 이 시점에 백업이 안 되어 있었다는 걸 깨닫고 식은땀이 났다. 다행히 늦지 않아서 백업을 진행했다.
# 1. 현재 conf 전체 백업
tar czf /root/httpd_conf_backup_$(date +%Y%m%d_%H%M).tar.gz /etc/httpd
# 2. 현재 로드 모듈 목록
httpd -M > /root/httpd_modules_before.txt 2>&1
# 3. 현재 RPM 패키지 목록
rpm -qa | grep -E "httpd|apr" > /root/httpd_rpm_list_before.txt
교훈: 백업은 작업 시작 전, 정확히는 "시스템에 영향을 주는 첫 명령(rpm -e)을 실행하기 직전"에 무조건 해야 한다. 작업 중간에 뒤늦게 깨닫고 백업하는 건 운이 좋아서 가능했던 것이지 권장되는 순서가 아니다. 이 백업 파일이 이후 6번 항목(conf 덮어쓰기 사고)을 복구하는 데 결정적인 역할을 했다.
7. HTTP/2 모듈 — 포기한 이유
configure 로그에 다음과 같이 떴다:
checking for nghttp2 version >= 1.2.1... FAILED
configure: WARNING: nghttp2 version is too old
checking whether to enable mod_http2... no (disabled)
시스템에 깔린 libnghttp2가 1.33.0으로, 버전 자체가 너무 낮아 mod_http2가 빌드되지 않았다. 이를 해결하려면 최신 nghttp2 라이브러리를 별도로 빌드해서 시스템에 올려야 하는데, 이번 작업의 본래 목적(CVE 대응)과는 무관한 부가 작업이라 HTTP/2 없이 진행하기로 결정했다. HTTP/2가 빠져도 HTTP/1.1로는 정상 동작하니 기능 손실은 아니고, 성능 최적화 기능 하나가 빠지는 것뿐이다.
참고: 운영 서버(ai-oep-web01)는 http2_module이 활성화되어 있어서, dev와 모듈 구성이 100% 동일하진 않게 됐다. 추후 필요해지면 nghttp2 신버전을 따로 반입해서 추가하면 된다.
8. RPM 제거 → make install — 진짜 삽질이 시작된 구간
systemctl stop httpd
rpm -e --nodeps httpd # httpd-tools, httpd-filesystem은 운영처럼 남겨둠
cd /usr/local/src/httpd-2.4.68
make install
삽질 #4 — make install이 운영 중이던 conf를 통째로 vanilla 기본값으로 덮어씀
make install 직후 httpd -k start를 시도했는데 SSL도, 프록시도, VirtualHost도 전혀 동작하지 않았다. httpd.conf를 열어보니 완전히 다른 내용 — vanilla conf가 들어가 있었다.
- SSLCertificateFile "/etc/httpd/ssl/wild/cert.pem"
- ProxyPass / http://127.0.0.1:3001/
- <VirtualHost *:443> ... tecampus.asianaidt.com ...
+ #LoadModule ssl_module modules/mod_ssl.so (주석 처리됨, SSL 자체가 꺼짐)
+ #LoadModule proxy_module modules/mod_proxy.so
+ DocumentRoot "/etc/httpd/htdocs"
원인: Apache 소스 빌드의 make install은 보통 기존 httpd.conf가 있으면 보존하고 새 버전은 .default로 따로 둔다고 알고 있었는데, 이번엔 그렇게 동작하지 않았고 운영 설정이 통째로 날아갔다.
→ 해결: 미리 떠둔 백업(7번 항목)에서 원본 conf를 복원.
cp /etc/httpd/conf/httpd.conf /etc/httpd/conf/httpd.conf.broken_$(date +%Y%m%d_%H%M)
tar xzf /root/httpd_conf_backup_*.tar.gz -C /tmp/conf_restore etc/httpd/conf/httpd.conf
cp /tmp/conf_restore/etc/httpd/conf/httpd.conf /etc/httpd/conf/httpd.conf
교훈: 백업이 없었다면 운영 설정(SSL 인증서 경로, VirtualHost, CORS 헤더, 프록시 대상 등 수십 줄)을 전부 손으로 다시 쳐야 했을 것이다. make install 전에 conf 백업은 선택이 아니라 필수다.
삽질 #5 — RPM이 소유했던 conf.modules.d/의 핵심 파일들이 같이 삭제됨
rpm -e --nodeps httpd로 패키지를 지우면서, conf.modules.d/ 디렉토리 안의 RPM 소유 파일들(보통 00-base.conf, 00-mpm.conf 등 — 핵심 모듈과 MPM을 로드하는 파일)이 같이 삭제됐다. 남은 건 직접 추가했던 00-ssl.conf, 10-h2.conf, 10-proxy_h2.conf 세 개뿐이었다.
이 때문에 mpm_event_module을 비롯한 핵심 모듈이 아예 로드되지 않았고, httpd -X(포그라운드 디버그 모드)를 실행하면 **에러도 없이 즉시 정상 종료(exit code 0)**되는 이상한 증상이 나타났다. MPM이 없으니 "할 일이 없다"고 판단해 조용히 끝난 것이다.
→ 해결: conf.modules.d/00-base.conf, 00-mpm.conf를 직접 재작성.
cat > /etc/httpd/conf.modules.d/00-base.conf << 'EOF'
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_host_module modules/mod_authz_host.so
... (생략, 필요한 모듈 전부)
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
EOF
cat > /etc/httpd/conf.modules.d/00-mpm.conf << 'EOF'
LoadModule mpm_event_module modules/mod_mpm_event.so
EOF
교훈: rpm -e로 패키지를 지우기 전에, 그 패키지가 소유한 파일 목록을 미리 뽑아두고(rpm -ql httpd > before.txt) 어떤 디렉토리/파일이 같이 사라질지 파악해야 한다. 특히 conf.modules.d/처럼 "설정 파일 같지만 실제로는 RPM이 관리하는" 디렉토리는 늦게 깨달으면 한참 헤매게 된다.
삽질 #6 — 비활성화된 http2 모듈이 남긴 잔재
문법 체크(httpd -t)에서 또 에러가 났다.
Cannot load modules/mod_http2.so into server: ... No such file or directory
원인: 7번 항목에서 http2를 빌드하지 않기로 했는데, conf.modules.d/10-h2.conf와 10-proxy_h2.conf가 옛 RPM 시절부터 남아있던 잔재라서 여전히 mod_http2.so를 로드하려고 시도했다.
→ 해결: 두 파일을 비활성화 (확장자를 바꿔서 Include conf.modules.d/*.conf에서 안 읽히게 함).
mv /etc/httpd/conf.modules.d/10-h2.conf /etc/httpd/conf.modules.d/10-h2.conf.disabled
mv /etc/httpd/conf.modules.d/10-proxy_h2.conf /etc/httpd/conf.modules.d/10-proxy_h2.conf.disabled
9. 진짜 마지막 보스 — PidFile 경로 버그
httpd -t는 Syntax OK가 나왔는데, 정작 httpd -X로 실행하면 또 **에러 메시지 없이 즉시 종료(exit code 1)**됐다. error_log에도 아무 기록이 없었다. 가장 답답했던 구간이다.
strace로 시스템 콜을 추적해서 원인을 찾았다.
strace -f -e trace=open,openat,chdir,setuid,setgid -o /tmp/httpd_strace.log /etc/httpd/bin/httpd -X
tail -40 /tmp/httpd_strace.log
마지막 줄에서 범인을 찾았다.
openat(AT_FDCWD, "/var/logs/httpd.pid.Kr0zBa", O_RDWR|O_CREAT|O_EXCL, 0600) = -1 ENOENT (No such file or directory)
+++ exited with 1 +++
원인: /var/log가 아니라 /var/logs(s가 붙은, 존재하지 않는 경로)에 PID 파일을 만들려고 시도하다가 실패해서 그대로 죽었다. configure 시 --localstatedir=/var를 줬을 때 PidFile 기본값이 이상하게 계산되면서 생긴 문제로 추정된다. conf에는 PidFile 지시어가 전혀 명시되어 있지 않았다(주석에서 단어만 언급될 뿐).
→ 해결: conf에 PidFile을 명시적으로 지정.
sed -i '/^ServerRoot "\/etc\/httpd"/a PidFile "logs/httpd.pid"' /etc/httpd/conf/httpd.conf
이걸 추가한 뒤 드디어 정상 기동됐다.
교훈: "에러 메시지 없이 조용히 죽는" 증상은 conf 문법 오류가 아니라 런타임 단계의 파일 시스템 접근 실패일 가능성이 높다. httpd -t는 문법만 검사하고 실제 포트 바인딩이나 PID 파일 생성은 시도하지 않기 때문에 통과해도 실행은 실패할 수 있다. 이런 경우 strace -f로 마지막 시스템 콜을 추적하는 게 가장 빠르고 확실한 디버깅 방법이다.
10. 기타 자잘한 삽질들
일반 사용자로 80/443 포트 바인딩 시도
[parkyi@ai-oep-dev bin]$ ./httpd -k restart
(13)Permission denied: AH00072: make_sock: could not bind to address [::]:80
1024 미만 포트는 root 권한이 필요하다. httpd의 마스터 프로세스는 root로 떠서 포트를 바인딩한 뒤, 자식 워커 프로세스만 conf의 User/Group 지시어로 권한을 낮추는 구조이기 때문에, 시작은 항상 root로 해야 한다.
모듈 중복 로드 경고
[so:warn] AH01574: module headers_module is already loaded, skipping
[so:warn] AH01574: module alias_module is already loaded, skipping
[so:warn] AH01574: module rewrite_module is already loaded, skipping
8번 항목에서 새로 작성한 00-base.conf와, 백업에서 복원한 원본 httpd.conf 양쪽에 같은 LoadModule 줄이 남아있어서 발생. 에러는 아니고 경고지만, 한쪽을 주석 처리해서 정리했다.
grep -rn "LoadModule.*headers_module\|LoadModule.*alias_module\|LoadModule.*rewrite_module" \
/etc/httpd/conf.modules.d/ /etc/httpd/conf.d/ /etc/httpd/conf/httpd.conf
# 중복 확인된 쪽만 주석 처리
apachectl은 systemd 서비스가 아니라 수동 스크립트로 관리되고 있었다
처음엔 운영 서버에 httpd.service가 당연히 있을 거라 생각하고 systemd 서비스 파일부터 옮겨오려 했지만,
[root@ai-oep-web01 ~]# systemctl status httpd
Unit httpd.service could not be found.
확인해보니 운영 서버는 systemd 등록 없이 수동으로 apachectl start를 실행해서 2월부터 쭉 띄워놓은 상태였다. dev도 동일하게 apachectl로 기동하는 방식을 따랐고, 재부팅 대비를 위해 rc.local에 등록해두는 것으로 보완했다.
echo '/etc/httpd/bin/apachectl start' >> /etc/rc.d/rc.local
chmod +x /etc/rc.d/rc.local
11. 최종 정리 — 전체 작업 순서 (요약)
같은 상황(RHEL8 + 망분리 + 운영서버는 소스 컴파일 구조)을 마주한다면 아래 순서를 추천한다.
- 운영 서버 정보를 먼저 전부 뽑는다: httpd -V, httpd -M, rpm -ql httpd(또는 미설치 여부), conf 구조, systemd 서비스 파일 유무, 모듈 디렉토리 구조
- dev 서버 백업: tar czf conf 전체, httpd -M, rpm -qa | grep httpd 결과 모두 저장
- 빌드 의존성 설치 가능 여부 확인 (dnf install gcc make pcre-devel ... ) — 내부 미러로 대부분 해결 가능
- httpd 소스 tarball만 외부에서 반입 (체크섬 검증 필수, 사이즈도 같이 대조)
- 운영과 동일한 레이아웃으로 configure — RPM 전용 패치 옵션(--runtimedir, --logfiledir 등)은 vanilla 소스에 없으니 빼야 함
- make 빌드
- httpd 중지 → rpm -e --nodeps httpd (tools/filesystem은 운영처럼 유지)
- make install
- conf 덮어쓰기 여부 확인 → 필요시 백업에서 복원
- conf.modules.d/ 핵심 모듈 로드 파일 확인/재작성 (RPM이 지웠을 가능성 큼)
- httpd -t로 문법 체크 → 에러 다 잡힐 때까지 반복
- httpd -X(포그라운드)로 실제 기동 테스트 — 에러 없이 조용히 종료되면 strace -f로 원인 추적
- 정상 확인 후 apachectl start로 정식 기동
- 재부팅 대비 자동 기동 등록 (systemd 또는 rc.local, 운영 방식에 맞춰서)
- 모듈 중복 경고 등 잔여 항목 정리
마무리
처음에는 단순히 "소스 받아서 configure, make, make install" 세 줄로 끝날 거라 생각했는데, 실제로는:
- RHEL이 패치한 비표준 옵션
- RPM 제거 시 같이 사라지는 RPM 소유 파일
- make install의 conf 덮어쓰기
- 잘못된 PidFile 경로 하드코딩
같은, 공식 문서에는 안 나오는 환경 특이적인 함정들이 줄줄이 나왔다. 다행히 작업 전에 떠둔 백업과 strace를 활용한 끝에 운영과 동일한 구조로 무사히 업그레이드를 마쳤다. 비슷한 환경에서 작업하시는 분들께 이 글이 시간을 아껴주길 바란다.