카테고리 보관물: WEB

Let’s Encrypt 인증서 생성 실패

개요

certbot을 사용해 Let’s Encrypt 인증서 생성 실패가 발생하는 원인은 다양하지만, RHEL/Rocky Linux 계열의 서버에서는 SELinux가 certbot의 HTTP 인증 프로세스를 차단하여 실패하는 경우가 있습니다. Apache가 설치된 환경에서는 Apache 자체는 SELinux 정책에 자동 등록되지만, certbot이 도메인 검증 시 사용하는 80/443 포트 바인딩용 독립 프로세스는 SELinux 정책에 등록되지 않아 차단됩니다. 이 글에서는 오류 내용과 SELinux 비활성화를 통한 해결 방법을 정리합니다.

증상 — certbot 실행 오류

certbot으로 인증서 발급을 시도하면 webroot 플러그인 관련 오류가 발생하며 인증서 발급이 완료되지 않습니다.

@:~# sudo certbot certonly --cert-name  -d www.
...
certbot.errors.PluginError: Every requested domain must have a webroot when using the webroot plugin.
2025-01-01 01:13:52,404:ERROR:certbot._internal.log:Every requested domain must have a webroot when using the webroot plugin.

원인 분석 — SELinux 정책 미등록

SELinux가 enforcing 모드로 활성화된 경우, Apache처럼 사전에 SELinux 정책이 등록된 서비스는 정상 동작하지만, certbot이 도메인 인증 시 새로 바인딩하는 포트(80, 443)용 프로세스는 정책이 없어 차단됩니다. 이 때문에 certbot이 ACME challenge 응답을 처리하지 못하고 실패합니다.

# SELinux 현재 상태 확인
@:~# sestatus
SELinuxfs mount:                /sys/fs/selinux
SELinuxfs status:                 enabled
SELinux mount directory:         /etc/selinux
Loaded policy name:              targeted
Current mode:                    enforcing

해결 방법 — SELinux 비활성화

SELinux 정책을 개별 추가하는 방법도 있으나, 서버 환경에서 SELinux 제어가 불필요한 경우 비활성화 처리가 가장 빠른 해결책입니다. /etc/sysconfig/selinux 파일에서 SELINUX=disabled로 변경한 뒤 재부팅합니다.

@:~# vi /etc/sysconfig/selinux
SELINUX=disabled

@:~# init 6

# 재기동 이후 SELinux 상태 확인
@:~# sestatus -v
SELinux status:                 disabled

SELinux 영구 비활성화 vs 임시 비활성화

/etc/sysconfig/selinux를 수정하고 재부팅하는 방식은 영구 비활성화입니다. 재부팅 없이 즉시 permissive 모드(정책 위반은 로깅만 하고 차단하지 않음)로 전환하려면 setenforce 0을 사용합니다. Permissive 모드는 SELinux 정책 개발이나 문제 진단 시 유용하며, 재부팅하면 /etc/sysconfig/selinux에 설정된 모드로 복귀합니다.

# 현재 세션에서만 permissive로 전환 (재부팅 후 원복)
setenforce 0

# 다시 enforcing으로 전환
setenforce 1

# 현재 상태 확인
getenforce

SELinux를 완전히 비활성화하는 대신 certbot에 대한 SELinux 정책만 추가하는 방법도 있습니다. audit.log에서 certbot 관련 AVC 거부 메시지를 확인하고 audit2allow 도구로 커스텀 정책 모듈을 생성할 수 있습니다. 그러나 프로덕션 서버에서 SELinux가 필수가 아닌 환경이라면 비활성화가 더 단순하고 확실한 해결책입니다.

결과 확인 — 인증서 재발급

SELinux 비활성화 후 certbot을 재실행하면 인증서가 정상 발급됩니다. 인증서 경로와 만료일을 확인합니다.

@:~# sudo certbot certonly --cert-name  -d www.
...
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live//fullchain.pem
Key is saved at:         /etc/letsencrypt/live//privkey.pem
This certificate expires on 2025-03-31.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

자동 갱신 확인

certbot은 인증서 발급 완료 시 --standalone 또는 --webroot 플러그인에 따라 systemd timer 또는 cron 기반 자동 갱신 태스크를 등록합니다. 자동 갱신이 정상 동작하려면 서버가 재부팅된 후에도 SELinux 상태가 disabled인지 확인해야 합니다. SELinux를 재활성화한 경우 certbot 갱신 시 다시 차단될 수 있습니다.

# systemd timer 확인 (RHEL 8+)
systemctl status certbot-renew.timer

# 갱신 dry-run 테스트
certbot renew --dry-run

# 인증서 만료일 확인
certbot certificates

참고

관련 포스트:

참고 문서: certbot standalone 플러그인 공식 문서 · RHEL SELinux 상태 변경 공식 문서

소스코드 플러그인 비교

개요

기술 블로그에서 코드를 게시글에 넣을 때 단순히 일반 텍스트로 삽입하면 가독성이 크게 떨어집니다. 들여쓰기가 무너지고, 문법 요소가 구분되지 않으며, 긴 줄은 줄 번호 없이 보기 어렵습니다. WordPress에서 소스코드를 게시글에 삽입하고 구문 강조(Syntax Highlight) 처리하기 위해 사용하는 소스코드 플러그인들을 비교합니다. Code Syntax Block, Enlighter, SyntaxHighlighter Evolved 등 주요 플러그인의 기능, 지원 언어, 편의성을 비교하여 환경에 맞는 플러그인 선택에 도움을 드립니다.

이 글에서는 Code Syntax Block, Enlighter, SyntaxHighlighter Evolved 세 플러그인을 비교하고, Enlighter의 다양한 테마를 같은 Python 코드로 시각적으로 비교합니다.

플러그인 비교 — 특징 및 추천

세 플러그인은 각각 다른 방향성을 가집니다. Code Syntax Block은 WordPress 5.0 이후의 블록 에디터(Gutenberg)와 완벽하게 통합되며 별도 설정 없이 바로 사용할 수 있습니다. 지원 언어는 제한적이지만 최신 WordPress 블록 기반 편집 경험에 가장 잘 맞습니다. 가볍고 추가 JavaScript 의존성이 없다는 것이 장점입니다.

Enlighter는 이 블로그에서 사용 중인 플러그인으로, 가장 많은 테마(Enlighter, Godzilla, Beyond, Classic, Mowtwo, Eclipse, Dracula, Monokai 등)를 제공합니다. 텍스트 에디터 환경에서도 잘 동작하며, EnlighterJSRAW 클래스를 가진 <pre> 블록만 있으면 JavaScript가 자동으로 렌더링합니다. 터미널/쉘 출력을 포함한 generic 언어 지원이 특히 인프라 관련 블로그에 유용합니다.

SyntaxHighlighter Evolved는 Alex Gorbatchev의 SyntaxHighlighter 라이브러리 기반으로 오랜 역사를 가집니다. 단축코드(shortcode) 방식([code lang="java"])으로 작성하며, 클래식 에디터와의 호환성이 좋습니다.

  • Code Syntax Block ( Python, Default )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Default )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Enlighter )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Godzilla )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Beyond )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Classic )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Mowtwo )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Eclipse )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Droide )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Minimal )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Atomic )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Rowhammer )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Bootstrap4 )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Dracula )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

  • Enlighter Sourcecode ( Python, Monokai )
def get_prime_nums(s, e):
    prime_nums = []

    for i in range(s, e + 1):
        flag = 1
        for j in range(2, i // 2 + 1):
            if i % j == 0:
                flag = 0
                break

        if flag == 1:
            prime_nums.append(i)

    return prime_nums

if __name__ == '__main__':
    s = int(input("Insert Start Number : "))
    e = int(input("Insert Finish Number : "))
#    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
"""
    print(get_prime_nums(s, e))
// 소수를 출력합니다.

Enlighter 테마 선택 기준

Enlighter는 사이트 전체 기본 테마를 WordPress 관리자 화면의 Enlighter 설정(Settings > Enlighter)에서 지정할 수 있습니다. 개별 코드 블록에서 data-enlighter-theme 속성으로 테마를 덮어쓸 수도 있습니다. 위에서 비교한 결과 개인적으로는 다크 계열의 Dracula 또는 Monokai 테마가 터미널 명령어와 함께 쓸 때 가독성이 높습니다. 라이트 테마를 선호한다면 Enlighter(기본)Classic이 깔끔합니다. 어두운 배경과 높은 대비를 원하면 GodzillaEclipse가 좋습니다.

참고

참고 문서: SyntaxHighlighter Evolved 플러그인 · Code Syntax Block 플러그인

아파치 웹서버 정보 숨기기

개요

웹 취약점 스캐너(Nikto, Nessus 등)나 공격자가 서버를 탐지할 때 가장 먼저 확인하는 것이 HTTP 응답 헤더의 서버 정보입니다. 노출된 버전 정보는 해당 버전에 존재하는 CVE 취약점을 직접 타겟팅하는 데 활용됩니다.

기본 설정된 Apache로 웹서비스를 운영하면 HTTP 응답 헤더와 에러 페이지에 웹서버 정보(버전, OS, PHP 버전)가 노출됩니다. 이 정보는 공격자에게 취약점 탐색 시 활용될 수 있으므로 ServerTokensServerSignature 지시자로 불필요한 정보를 숨기는 것이 보안 운영의 기본입니다.

증상 — 기본 상태에서의 서버 정보 노출

curl로 HTTP 응답 헤더를 확인하면 Apache 버전, OS 정보, PHP 버전까지 노출되는 것을 확인할 수 있습니다.

<admin-user>@localhost:~$ curl --head <domain>
HTTP/1.1 302 Found
Date: Fri, 05 Mar 2021 02:21:54 GMT
Server: Apache/2.4.x (CentOS) OpenSSL/1.0.x PHP/7.2.x
// 웹서버/OS 버전 외에도 PHP 버전까지 노출되고 있습니다.

httpd.conf 설정

httpd.conf 또는 httpd-default.conf에 아래 두 지시자를 추가하고 Apache를 재시작합니다.

# httpd.conf 또는 httpd-default.conf에 추가
ServerTokens Prod
ServerSignature Off

# 아파치 재기동
# systemctl restart httpd

ServerTokens 옵션 상세

ServerTokens 지시자의 각 옵션별로 HTTP 응답 헤더에 포함되는 정보 범위가 다릅니다. 보안을 위해 가장 최소한의 정보만 노출하는 Prod 옵션 사용을 권장합니다.

키워드 제공 정보 예문
Prod 웹 서버 종류 Apache
Min 웹 서버 버전 Apache/2.2.3
OS 웹 서버 버전 + 운영체제 Apache/2.2.3 (CentOS)
Full 웹 서버의 모든 정보 Apache/2.2.3 (CentOS) DAV/2 PHP/5.16
ServerTokens 지시자 옵션

추가 보안 설정 — 에러 페이지 커스터마이징

ServerSignature Off로도 Apache 기본 에러 페이지에서 버전 정보가 노출될 수 있습니다. 404, 403, 500 등 에러 발생 시 기본 Apache 에러 페이지에는 서버 서명이 포함되므로, 커스텀 에러 페이지를 사용하는 것이 보안에 유리합니다.

# httpd.conf에 커스텀 에러 페이지 지정
ErrorDocument 403 /error/403.html
ErrorDocument 404 /error/404.html
ErrorDocument 500 /error/500.html

또한 Apache의 Header 지시자(mod_headers)로 보안 관련 HTTP 응답 헤더를 추가하면 XSS, 클릭재킹 등의 공격에 대한 방어가 강화됩니다. X-Content-Type-Options: nosniff는 MIME 타입 스니핑을 차단하고, X-Frame-Options: SAMEORIGIN은 iframe을 이용한 클릭재킹을 방지합니다. Strict-Transport-Security(HSTS)는 HTTPS 강제 적용을 브라우저에 알립니다.

# mod_headers 활성화 후 적용 (httpd.conf 또는 .htaccess)
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
# HTTPS 환경에서만 HSTS 적용
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

이러한 보안 헤더 설정은 securityheaders.com 등의 도구로 점수를 측정하고 누락된 헤더를 파악하는 방법으로 체계적으로 관리할 수 있습니다.

결과 확인

Apache 재시작 후 동일하게 curl로 헤더를 확인하면 Server: Apache만 표시되어 버전 및 OS 정보가 제거된 것을 확인할 수 있습니다.

<admin-user>@localhost:~$ curl --head <domain>
HTTP/1.1 302 Found
Date: Fri, 05 Mar 2021 02:26:10 GMT
Server: Apache
// 웹서버 종류만 표시

참고

참고 문서: Apache ServerTokens 지시자 공식 문서 · Apache ServerSignature 지시자 공식 문서