Docker Alpine Linux에 WordPress용 Haproxy 설치 후 SSL 인증서 적용하기#4

Certbot의 DNS plugin을 이용하여 SSL 인증서를 자동갱신가능하게 발급받고, WordPress 앞단의 Haproxy(L4)에 SSL 인증서를 설치하여 Redirect와 autoscale의 기반을 마련하는 방법을 설명한다.

1. 환경설명

우리는 지금까지 HostOS(ubuntu)에 WordPress 컨테이너(alpine,lihttpd)와 MairaDB(alpine) 컨테이너를 만들었다.
이번에는 동일한 HostOS에 Haproxy(alpine) 로드밸런서를 추가로 설치해서 SSL Redirect 기능을 구현할 예정이다.

2. 도메인 네임서버 변경 및 api 토큰 확인

Certbot 에서는 무료 SSL 인증서를 발급받을 수 있는데, 유효기간이 90일밖에 되지않아서, 매번 수동으로 갱신하기엔 번거롭다.
도메인 소유에 대한 인증방식이 여러개 있고, 자동갱신을 지원해주는 방식도 여럿 있어 보였다.

webroot: 웹 URI(/.well-known/acme-challenge)에 특정 Key값이 들어가는걸로 보여지는데, 우리는 wordpress 앞단의 L4에서 사용할 예정(haprox는 웹서버 수정권한이 없으므로)이니 이 방식은 맞지 않다.
DNS TXT: 도메인 TXT 레코드(_acme-challenge)에 특정 Key값이 들어가는데 이것도 마찬가지로 매번 수동은 어렵다.
Nginx plugin: 이건 자동갱신이 되는것 같은데 우리는 관리편의를 위해 Haproxy 한곳에서만 사용할 예정이니 맞지 않다.

그래서 찾은게 DNS Plugin이다.(https://eff-certbot.readthedocs.io/en/stable/using.html#dns-plugins)
요약하자면, dns api를 제공하는 route53 또는 cloudflare의 네임서버를 사용해서 api 호출 권한을 certbot쪽에 전달하면 자동갱신이 가능하다.
route53은 적절한 권한을 부여한 accesskey를 발급해야하고, 비용이 발생하므로, 제외.
cloudflare는 무료에다가 api token발급이 굉장히 심플하다.

가비아에서 도메인을 구매해서 무료 네임서버를 이용중이었는데, 바로 cloudflare로 변경하였다.
순서는 간단하다. cloudflare 가입 -> 네임서버 주소 확인 -> 가비아에서 도메인 네임서버 주소 변경(본인인증 필요) -> cloudflare website 추가
약 1시간정도 기다리면 네임서버 이전이 완료되고 우측 상단의 내 프로필에서 global api key 를 확인한다.

haproxy1

3. SSL 인증서 및 관련정보 저장용 디렉토리 생성

				
					#/docker/certbot/data/live 폴더안에 발급된 도메인이름으로 저장된다.
mkdir -p /docker/certbot/data
#컨테이너의 /var/lib/letsencrypt 폴더와 마운트
mkdir -p /docker/certbot/backup
#certbot log
mkdir -p /docker/certbot/log
				
			

4. certbot/dns-cloudflare 일회성 컨테이너를 활용하여 SSL 인증서 발급 받기

				
					#cloudflare api key credentials 파일 생성
vi /docker/certbot/data/cloudflare_cred.ini
				
			
haproxy2
				
					dns_cloudflare_api_key = {your-apikey}
dns_cloudflare_email = {your-email}
				
			
				
					# 인증서 발급
docker run -it --rm --name certbot \
-v '/docker/certbot/data:/etc/letsencrypt' \
-v '/docker/certbot/backup:/var/lib/letsencrypt' \
-v '/docker/certbot/log:/var/log/letsencrypt' \
certbot/dns-cloudflare certonly -d {your-domain} \
--dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare_cred.ini \
--email {your-email} --agree-tos --no-eff-email
				
			

–rm = 실행완료 후 이미지는 남지만 컨테이너는 삭제된다.
{your-domain} = test3.infraadmin.tech 형식
{your-email} = a@a.com 형식
–dns-cloudflare-credentials /etc/letsencrypt/cloudflare_cred.ini = cloudflare API key 인증
–agree-tos = 사용약관 동의
–no-eff-email = 광고(?)메일 수신 안함

아래 화면처럼 Unsafe(크리덴셜 파일의 퍼미션)경고가 뜨지만 발급이 잘되고, /docker/certbot/data/live에 잘 생성되었다.

haproxy3
haproxy4

5. HAproxy에서 사용할 수 있게 변환.

				
					mkdir -p /docker/haproxy/config
cat /docker/certbot/data/live/{your_domain}/cert.pem \
/docker/certbot/data/live/{your_domain}/chain.pem \
/docker/certbot/data/live/{your_domain}/privkey.pem \
> /docker/haproxy/config/{your_domain}.pem
				
			

Key 파일을 같이 합치게 되는데 , 공식문서에서는 아래와 같이 설명하고 있다.(분리해서 제공도 가능하다)

HTTPS traffic is decrypted using the file ssl.pem, which contains both the site’s public SSL certificate and its private key.
https://www.haproxy.com/documentation/hapee/latest/security/tls/
https://www.haproxy.com/blog/redirect-http-to-https-with-haproxy/

6. 갱신 자동화

dry-run 옵션을 이용해 갱신 성공여부를 확인하고 Shell 스크립트를 작성하여 HostOS의 Crontab에 등록하여 주기적으로 갱신되게 구성한다. 우리는 haproxy에서 사용할 예정이므로 haproxy에서 사용가능한 형태로 변환하는 작업까지 자동화 한다.

				
					docker run --rm --name certbot \
-v '/docker/certbot/data:/etc/letsencrypt' \
-v '/docker/certbot/backup:/var/lib/letsencrypt' \
-v '/docker/certbot/log:/var/log/letsencrypt' \
certbot/dns-cloudflare certonly -d {your_domain} \
--dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare_cred.ini --dry-run
				
			

갱신할때는 -it 옵션, –email {your_email}, –agree-tos, –no-eff-email 옵션이 필요없다.
테스트 이므로 맨 마지막에 –dry-run을 잊지말자.(성공여부만 확인할뿐 성공하더라도 인증서File을 저장하지 않는다.)

The dry run was successful. 되면 성공이다.

아래와 같이 자동갱신 스크립트를 작성해서 crontab에 job으로 등록한다.
(HostOS(ubuntu)에 작성하여 HostOS의 Crontab에 등록)

				
					vi /docker/certbot/data/certbot_ssl_renew.sh
				
			
				
					#!/bin/bash
docker run --rm --name certbot \
-v '/docker/certbot/data:/etc/letsencrypt' \
-v '/docker/certbot/backup:/var/lib/letsencrypt' \
-v '/docker/certbot/log:/var/log/letsencrypt' \
certbot/dns-cloudflare certonly -d {your_domain} \
--dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare_cred.ini

#haprox에서 사용하는 SSL 인증서 형태로 변환하여 haproxy에서 사용할 ssl 폴더로 복사한다.
cat /docker/certbot/data/live/{your_domain}/cert.pem \
/docker/certbot/data/live/{your_domain}/chain.pem \
/docker/certbot/data/live/{your_domain}/privkey.pem \
> /docker/haproxy/config/{your_domain}.pem
				
			
				
					chmod 755 /docker/certbot/data/certbot_ssl_renew.sh
				
			
				
					vi /etc/crontab
				
			
haproxy5
				
					0 0 1 * * root /docker/certbot/data/certbot_ssl_renew.sh
				
			

7. haproxy 설정용 디렉토리 생성

				
					#Dockerfile 및 haproxy.cfg,  SSL 인증서 넣는 위치
#Certbot 인증서 저장때 이미 만들어 두었다.
#mkdir -p /docker/haproxy/config

				
			

8. Haproxy Dockerfile

				
					#alpine 3.15버전을 베이스로 한다.
FROM alpine:3.15
RUN apk add haproxy
COPY haproxy.cfg /etc/haproxy/

#certbot에서 발급받은 SSL 인증서를 /docker/haproxy/config 폴더에 이미 만들어 두었다.
COPY {your-domain}.pem /etc/ssl/certs

#haproxy start 스크립트
COPY start_haproxy.sh /home

ENTRYPOINT sh /home/start_haproxy.sh
				
			

9. Haproxy.cfg

				
					#---------------------------------------------------------------------
# Example configuration for a possible web application.  See the
# full configuration options online.
#
#   http://haproxy.1wt.eu/download/1.5/doc/configuration.txt
#
#---------------------------------------------------------------------

#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
    # to have these messages end up in /var/log/haproxy.log you will
    # need to:
    #
    # 1) configure syslog to accept network log events.  This is done
    #    by adding the '-r' option to the SYSLOGD_OPTIONS in
    #    /etc/sysconfig/syslog
    #
    # 2) configure local2 events to go to the /var/log/haproxy.log
    #   file. A line like the following can be added to
    #   /etc/sysconfig/syslog
    #
    #    local2.*                       /var/log/haproxy.log
    #
    log         127.0.0.1 local2

    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     4000
    user        haproxy
    group       haproxy
    daemon

    # turn on stats unix socket
    stats socket /var/lib/haproxy/stats
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    option http-server-close
    option forwardfor       except 127.0.0.0/8
    option                  redispatch
    retries                 3
    timeout http-request    10s
    timeout queue           1m
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    timeout http-keep-alive 10s
    timeout check           10s
    maxconn                 3000


listen stats
    bind :4331
    stats enable
    stats realm Haproxy
    stats uri /haproxy_stats

#---------------------------------------------------------------------
# main frontend which proxys to the backends
#---------------------------------------------------------------------
frontend mysite                                                    
    mode http                                                         
    bind *:80                                                         
    bind *:443 ssl crt /etc/ssl/certs/{your-domin}.pem             
    http-request redirect scheme https unless { ssl_fc }             
    http-request set-header X-Forwarded-Proto https if { ssl_fc }     
    default_backend wordpress                                               
                                                                      
#---------------------------------------------------------------------
# round robin balancing between the various backends                  
#---------------------------------------------------------------------
backend wordpress                                                           
    balance     roundrobin                                            
    server  app1 {your-wordpress-ip}:81 check

				
			
  • hostIP:4331/haproxy_stats = 로드밸런싱 상세정보를 제공해주는 페이지
  • bind *:443 ssl crt /etc/ssl/certs/{your-domin}.pem = https에서 사용할 인증서 지정
  • http-request redirect scheme https unless { ssl_fc } = Client(broser)로 영구이동(301)을 리턴.
  • http-request set-header X-Forwarded-Proto https if { ssl_fc } = WordPress 어플리케이션 구조상 https를 전달하지 않으면 admin 페이지의 Mixed Content 에러가 발생. (아래 wp-setting.php 수정작업과 관련있음)
  • server app1 {your-wordpress-ip}:81 check = 우리는 하나의 HostOS(ubuntu)에서 http를 사용하는 컨테이너를 여러대 사용하기 때문에 포트가 겹치면 안된다. (아래 기존 lighttpd.conf의 포트 변경작업과 관련있음)
  • https://www.haproxy.com/documentation/hapee/latest/traffic-routing/redirects/
  • https://www.haproxy.com/documentation/hapee/latest/traffic-routing/rewrites/rewrite-requests/

10. start_haproxy.sh

				
					haproxy -f /etc/haproxy/haproxy.cfg
/bin/sh
				
			

CMD [“haproxy”, “-f”, “/etc/haproxy/haproxy.cfg”] 를 사용하여 foreground로 실행해서 PID가 죽으면 컨테이너도 같이 죽어서 알림을 받는 방법을 지향하지만 Haproxy는 공식적으로 alpine 이미지를 지원하고 있지 않다.(공식지원은 데비안 버전이며, alpine  공식버전은 아니고 관련 링크만 제공한다.)

Alpine에서의 haproxy는 CMD 방식으로 실행하면 컨테이너가 바로 죽는 현상이 발생하고, 로그도 남지 않는다. -d 옵션(debug)을 주고 실행하면 실행은 되지만 많은 로그를 남길거고 그로인해 데이터가 많이 누적이 될 수 있다. CMD를 build에 넣지 않고 run(-it /bin/sh 옵션포함) 한뒤에 exce로 들어가서 다시 실행하면 또 잘 동작한다. 

만약 이글을 읽고 있을때 haproxy 공식 alpine 이미지가 있다면 해당 이미지를 사용하는게 좋을듯 하다.
debian base의 공식이미지로 테스트를 했을때는 haproxy.cfg 수정 후 graceful reload가 가능하다.(가용성 측면에서 LB의 가장 큰기능)

11. 작업 순서

  • WordPress 관리자 메뉴에서 워드프레스 주소, 사이트 주소를 https://{your-domain} 으로 변경한다.
    (변경 직후 접속이 끊어진다.)
  • 컨테이너를 종료한 후 lihttpd.conf 파일에서 server port를 변경한다.(server.port = 81)
  • WordPress wp-config.php 파일에 아래와 같이 어드민 페이지에서 https를 사용가능하게 상단에 추가한다.
				
					 /* @package WordPress
 */
// ** hobumnim ssl */
define( 'CONCATENATE_SCRIPTS', false);

define( 'FORCE_SSL_LOGIN', true);
define( 'FORCE_SSL_ADMIN', true);
define( 'SCRIPT_DEBUG', true);

if ($_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
        $_SERVER['HTTPS']='on';
// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
				
			
  • wordpress 컨테이너를 다시 실행하고, haproxy를 실행하면 http로 접근시 인증서가 적용된 https로 리다이렉트되며, 워드프레스에서 관리하는 이미지 파일, css파일등등이 https로 변경되어 있음을 확인할 수 있다.

12. 어드민 페이지 접근 제한(Haproxy)

최소한의 보안을 위해 어드민 페이지를 특정 아이피에서만 접근가능하게 설정해 두는게 좋다.
아래는 워드프레스 주소가 https://{your-domain}/wordpress 일때 특정 아이피만 접근을 허용하는 Haproxy 설정이다.

				
					frontend mysite                                                    
    mode http                                                         
    bind *:80
    bind *:443 ssl crt /etc/ssl/certs/{your-domin}.pem            
    http-request redirect scheme https unless { ssl_fc }             
    http-request set-header X-Forwarded-Proto https if { ssl_fc }     
    default_backend wordpress                                               
                                                                      
#---------------------------------------------------------------------
# round robin balancing between the various backends                  
#---------------------------------------------------------------------
backend wordpress                                                           
    balance     roundrobin                                            
    server  app1 172.17.0.2:81 check
    acl admin_allowed src {your-ip}
    acl restricted_page path_beg /wp-admin
    acl restricted_page path_beg /wordpress/wp-login.php

    http-request deny if restricted_page !admin_allowed
				
			

13. 어드민 페이지 접근 제한(lighttpd.conf 제일 아래 추가)

만약 설계구조상 wordpress 자체에서 제어하고 싶다면 lighhtpd.conf 파일에 추가해 준다.

				
					#여러 IP를 등록할때
$HTTP["remoteip"] !~ "{your-ip}|{your-ip}|{your-ip}" {
#하나의 IP를 등록할때
#$HTTP["remoteip"] == "{your-ip}" {
    $HTTP["url"] =~ "^/wordpress/wp-login.php|^/wordpress/wp-admin/" {
      url.access-deny = ( "" )
    }
}
				
			

14. 사이트주소에서 wordpress를 제거 했을때.(lighttpd.conf 제일 아래 추가)

				
					url.rewrite = (
"^/wordpress/(wp-.+).*/?" => "$0",
"^/(.*)\.(.+)$" => "$0",
"^/(.+)/?$" => "/index.php/$1"
				
			

15. haproxy 캐시 사용

16. stats