Vault의 프로덕션 배포와 데모 배포의 차이점은 고가용성 보장과 Unseal Key 보호 그리고 TLS 인증서 적용입니다.
고가용성 보장을 위해서는 Vault 클러스터와 Load balnacer를 통합하고, Unseal Key 보호를 위한 물리적으로 보호되는 금고와 키 분배 그리고 재해 복구 시나리오의 고려가 필요합니다.
이번 포스트에서는 Vault 클러스터 구성과 Envoy L7 Load Balancer 구축을 다루며, PKI와 Unseal Key Best Practice는 차후에 다른 포스트에서 다룰 예정입니다.
이 글을 다 읽고 나면, 다음과 같은 지식을 습득하게 될 것입니다.
(출처: HashiCorp - Vault with integrated storage reference architecture)
위 그림은 HashiCorp에서 권장하는 프로덕션 Vault 배포 아키텍처의 개요를 보여줍니다.
최소 5개의 Vault 인스턴스가 클러스터를 구성하고, Load Balancer로 엔드포인트 진입점의 고가용성을 확보합니다.
5대의 인스턴스가 필요한 이유는 Vault 스토리지 백엔드가 쿼럼 구성을 위해 Raft 알고리즘을 사용하고, 5대가 되어야 Raft 합의를 위한 정족수를 깨지 않고도 두 대의 노드에 대한 장애 조치를 허용하기 때문입니다.
만약 3대의 인스턴스로 Vault를 구성하면, Leader 선출 상황에서 잠시 경합이 발생할 수 있으며, 네트워크가 조금만 불안정해져도 요청 처리가 중단될 수 있습니다.
또한, Vault 노드와 로드 밸런서는 서로 다른 가용성 존에 분배되어야 합니다. 완전히 배포된 Vault의 중요도를 감안하면 Vault 클러스터는 지역 수준의 장애에 대한 내결함성을 가져야 하며 이를 위해서는 AZ를 분리하는 것이 중요합니다.
HashiCorp는 Raft 합의를 위해 노드 간 지연시간을 8ms 이내로 유지할 것을 권고하지만, 부하 수준에 따라 더 높은 레이턴시가 허용될 수 있습니다.
높은 부하를 가지는 엔터프라이즈 클러스터에서, 이러한 배포 시나리오에 대응하는 최선의 방법은 Vault의 Performance Replicator 클러스터를 배포하는 것입니다. 이것은 엔터프라이즈 라이센스 전용 기능이며, OSS에서는 사용할 수 없습니다.
다음으로, 모든 '쓰기' 작업이 Active 노드에서만 일어난다는 것을 기억해야 합니다.
Raft 합의 알고리즘을 통해 내장 DB에 데이터를 저장하는 모든 쿼리가 '쓰기' 작업으로 간주되기 때문에, 쓰기 작업의 범주가 예상보다 훨씬 넓을 수 있습니다.
'읽기' 작업의 경우에는 Performance Standby Node를 구성하여 부하를 분산할 수 있지만, 이것도 엔터프라이즈 라이센스 전용 기능입니다.
저희 회사에서는 오픈 소스 에디션을 사용하여 다음과 같은 구성으로 Vault 클러스터를 배포했습니다.
HashiCorp는 Vault 노드를 컨테이너보다는 가상머신으로, 가상머신보다는 베어메탈 머신으로 구성할 것을 권고합니다.
이는 플랫폼의 보안 취약점이 그 플랫폼 위에서 동작하는 Vault 인스턴스에 영향을 줄 수 있기 때문입니다.
저희는 하이퍼바이저의 보안이 충분히 강화되어 있고, 사외의 워크로드를 호스팅 하는 환경이 아니기 때문에 하이퍼바이저 탈출 취약점에 의한 보안 침해 가능성이 제한적이라고 판단하여 VM으로 Vault를 배포했습니다.
조직의 보안 요구사항과 가용 예산에 따라 적절한 방법을 선택하는 것이 바람직합니다.
Vault 프로덕션 배포를 위해서는 다음과 같은 준비물이 필요합니다.
전반적인 구성 절차는 인터넷 연결이 가능하거나, Repository Proxy가 구성되어 있다는 가정 하에 설명됩니다.
TLS 인증서 체인은 이미 구성되어 있는 사설 PKI를 사용하거나, 외부 PKI를 이용해 발급할 수 있습니다.
인증서 체인 파일은 일반적인 리눅스 환경의 애플리케이션과 같이 위에서부터 아래로 Leaf Cert -> Intermediate CA -> Root CA 순으로 구성되어야 하며, CA Chain은 Intermediate CA -> Root CA 순으로 구성되어야 합니다.
# 전체 Vault 노드에서 순차 실행
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install vault
cp /opt/vault/tls/vault-ca-chain.pem /etc/pki/ca-trust/source/anchors/
update-ca-trust extract
# /etc/vault.d/vault.hcl
cluster_addr = "https://<LOCAL_IPV4_ADDRESS>:8201"
api_addr = "https://<LOCAL_IPV4_ADDRESS>:8200"
disable_mlock = true
ui = true
listener "tcp" {
address = "<LOCAL_FQDN_ADDRESS>:8200"
tls_cert_file = "/opt/vault/tls/vault-cert.pem"
tls_key_file = "/opt/vault/tls/vault-key.pem"
# tls_client_ca_file = "/opt/vault/tls/vault-ca-chain.pem" # for Client Cert Auth only
x_forwarded_for_authorized_addrs = "<Comma-separated-LB-IP-Addrs>"
tls_min_version = "tls13"
}
storage "raft" {
path = "/opt/vault/data"
node_id = "<UNIQUE_ID_FOR_THIS_HOST>"
retry_join {
leader_tls_servername = "<VALID_TLS_SERVER_NAME>"
leader_api_addr = "https://<ADDRESS_OF_PEER_1>:8200"
leader_ca_cert_file = "/opt/vault/tls/vault-ca-chain.pem"
leader_client_cert_file = "/opt/vault/tls/vault-cert.pem"
leader_client_key_file = "/opt/vault/tls/vault-key.pem"
}
retry_join {
leader_tls_servername = "<VALID_TLS_SERVER_NAME>"
leader_api_addr = "https://<ADDRESS_OF_PEER_2>:8200"
leader_ca_cert_file = "/opt/vault/tls/vault-ca-chain.pem"
leader_client_cert_file = "/opt/vault/tls/vault-cert.pem"
leader_client_key_file = "/opt/vault/tls/vault-key.pem"
}
retry_join {
leader_tls_servername = "<VALID_TLS_SERVER_NAME>"
leader_api_addr = "https://<ADDRESS_OF_PEER_3>:8200"
leader_ca_cert_file = "/opt/vault/tls/vault-ca-chain.pem"
leader_client_cert_file = "/opt/vault/tls/vault-cert.pem"
leader_client_key_file = "/opt/vault/tls/vault-key.pem"
}
retry_join {
leader_tls_servername = "<VALID_TLS_SERVER_NAME>"
leader_api_addr = "https://<ADDRESS_OF_PEER_4>:8200"
leader_ca_cert_file = "/opt/vault/tls/vault-ca-chain.pem"
leader_client_cert_file = "/opt/vault/tls/vault-cert.pem"
leader_client_key_file = "/opt/vault/tls/vault-key.pem"
}
retry_join {
leader_tls_servername = "<VALID_TLS_SERVER_NAME>"
leader_api_addr = "https://<ADDRESS_OF_PEER_5>:8200"
leader_ca_cert_file = "/opt/vault/tls/vault-ca-chain.pem"
leader_client_cert_file = "/opt/vault/tls/vault-cert.pem"
leader_client_key_file = "/opt/vault/tls/vault-key.pem"
}
}
<VALID_TLS_SERVER_NAME>
에는 각 노드의 FQDN을 입력하면 되며, Vault의 서버 인증서의 위치는 /opt/vault/tls/
입니다.
물론, 인증서 디렉토리는 환경에 맞춰 적절하게 변경해도 무방합니다.
## Vault 서비스 실행
systemctl enable vault
systemctl start vault
systemctl status vault
## Firewalld 포트 허용
sudo firewall-cmd --permanent --add-port=8200/tcp
sudo firewall-cmd --reload
sudo firewall-cmd --permanent --add-port=8201/tcp
sudo firewall-cmd --reload
## Vault 상태 확인
export VAULT_CACERT=/opt/vault/tls/vault-ca.pem
export VAULT_ADDR=https://<VAULT_FQDN>:8200
vault status
### Swap 비활성화
swapoff -a
sed -i '/\/swap/ s/^/#/' /etc/fstab
reboot
### SSHD 비활성화
systemctl disable sshd
systemctl stop sshd
### 코어 덤프 비활성화
bash -c 'echo "* hard core 0" >> /etc/security/limits.conf'
### 쉘 히스토리 비활성화
echo 'export HISTSIZE=0 HISTFILE=' >> ~/.bashrc
source ~/.bashrc
위 조치사항 중, SSHD 비활성화는 Vault 초기화가 완료된 다음 진행하는 것이 좋습니다.
모든 Vault 노드에서 Vault 서비스가 실행 중이며, Raft 클러스터의 리더 선출이 완료된 뒤 하나의 노드에서 Vault의 최초 초기화를 진행합니다.
export VAULT_CACERT=/opt/vault/tls/vault-ca.pem
export VAULT_ADDR=https://<VAULT_FQDN>:8200
vault operator init
이 작업은 전체 클러스터에서 한 번만 수행하면 되며, 초기화 후 콘솔에 Unseal Key와 Root Token이 출력되기 때문에 다이렉트 콘솔이 아닌 SSH 터미널에서 초기화 작업을 수행하는 것을 권장합니다.
초기화 된 Vault 클러스터는 개별 노드마다 각각 Unseal을 통해 사용 가능한 상태로 만들어 주어야 합니다.
Vault는 최초 시작 및 재시작 시 Sealing 상태로 시작됩니다. 이 상태에서는 스토리지에 저장된 암호화 된 데이터에 접근할 수 없으며, 운영자가 수동으로 Unseal 키를 입력하거나, KMS를 통해 Auto-Unsealing을 구성하여 인증을 위임해야 합니다 (엔터프라이즈 라이센스 필요)
Vault의 마스터 키는 Shamir's Secret Sharing (SSS)을 이용해 만들어지며, Unseal을 위해서는 총 5개의 키 중 3개를 입력해야 합니다.
공격자 또는 내부자가 혼자서 잠긴 Vault를 Unseal 할 수 없도록 Unseal 키는 물리적으로 분산된 장소에 보관해야 하며, 서로 다른 잠금 장치로 보호되어야 합니다. 이를 위한 Best Practice는 차후 Vault PKI 구축을 다루면서 자세하게 설명할 것입니다.
Vault Web UI가 활성화 되어 있을 경우, 위와 같이 웹 페이지를 통해 Unseal Key를 입력할 수 있습니다.
전체 노드에 대한 Unsealing이 끝났다면, 이제 Vault를 위한 로드 밸런서를 배포할 차례입니다.
# 전체 LB 노드에서 순차 실행
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
dnf install -y docker-ce
systemctl start docker
systemctl enable docker
docker pull envoyproxy/envoy:v1.34-latest
mkdir -p /opt/envoy
vi /opt/envoy/config-tls.yaml
/opt/envoy/config-tls.yaml (L7):
static_resources:
listeners:
- name: https_listener
address:
socket_address:
address: 0.0.0.0
port_value: 443
listener_filters:
- name: "envoy.filters.listener.tls_inspector" # necessary to use SNI header
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
filter_chains:
- filter_chain_match:
server_names: ["vault.sjuhwan.lab"]
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_params:
tls_minimum_protocol_version: TLSv1_3
tls_maximum_protocol_version: TLSv1_3 # Enforce TLS 1.3 on the client side
tls_certificates:
- certificate_chain:
filename: "/etc/envoy/tls/vault/vault-crt-chain.pem"
private_key:
filename: "/etc/envoy/tls/vault/vault-key.pem"
filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: vault_https
codec_type: AUTO
use_remote_address: true # Enable X-Forwarded-For and related headers
route_config:
name: vault_route
virtual_hosts:
- name: vault
domains: ["vault.sjuhwan.lab"]
routes:
- match:
prefix: "/"
route:
cluster: hashicorp-vault
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: hashicorp-vault
connect_timeout: 0.5s
type: STRICT_DNS
dns_lookup_family: V4_ONLY
lb_policy: round_robin
load_assignment:
cluster_name: hashicorp-vault
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: vault01.sjuhwan.lab
port_value: 8200
- endpoint:
address:
socket_address:
address: vault02.sjuhwan.lab
port_value: 8200
- endpoint:
address:
socket_address:
address: vault03.sjuhwan.lab
port_value: 8200
- endpoint:
address:
socket_address:
address: vault04.sjuhwan.lab
port_value: 8200
- endpoint:
address:
socket_address:
address: vault05.sjuhwan.lab
port_value: 8200
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: vault.sjuhwan.lab
common_tls_context:
tls_certificates:
- certificate_chain:
filename: "/etc/envoy/tls/vault/vault-crt-chain.pem"
private_key:
filename: "/etc/envoy/tls/vault/vault-key.pem"
validation_context:
match_typed_subject_alt_names:
- san_type: DNS
matcher:
exact: "vault01.sjuhwan.lab"
- san_type: DNS
matcher:
exact: "vault02.sjuhwan.lab"
- san_type: DNS
matcher:
exact: "vault03.sjuhwan.lab"
- san_type: DNS
matcher:
exact: "vault04.sjuhwan.lab"
- san_type: DNS
matcher:
exact: "vault05.sjuhwan.lab"
trusted_ca:
filename: "/etc/envoy/tls/vault/vault-ca-chain.pem"
health_checks:
- timeout: 2s
interval: 1s
unhealthy_threshold: 1
healthy_threshold: 1
tls_options:
alpn_protocols: "http/1.1"
http_health_check:
path: "/v1/sys/health"
common_lb_config:
healthy_panic_threshold:
value: 0.0
admin:
access_log_path: "/dev/null"
address:
socket_address:
address: 0.0.0.0
port_value: 8001
이해를 돕기 위해 실제 Lab 환경에서 사용한 구성 예제를 그대로 가져왔습니다.
*.sjuhwan.lab
이 들어간 필드의 FQDN을 환경에 맞춰 적절히 변경하면 되며, 몇 가지 구성 상 특이사항을 짚고 넘어가겠습니다.
기본적으로 HTTP/8001 포트를 통해 Envoy의 관리자 페이지에 접근할 수 있으며, 필요한 경우 비활성화 할 수 있습니다.
Envoy의 가장 큰 장점 중 하나가 Admin API를 통한 무중단 설정 변경이기 때문에, 개인적으로는 적절하게 보안을 강화하여 사용하는 것을 권장합니다.
docker run --rm \
-v /opt/envoy/config-tls.yaml:/config-tls.yaml \
-v /opt/envoy/tls/vault/:/etc/envoy/tls/vault \
envoyproxy/envoy:v1.34-latest --mode validate -c config-tls.yaml
docker run -d --name envoy -p "0.0.0.0:443:443" -p "0.0.0.0:8200:8200" -p "0.0.0.0:8001:8001" \
-v /opt/envoy/config-tls.yaml:/config-tls.yaml \
-v /opt/envoy/tls/vault/:/etc/envoy/tls/vault \
envoyproxy/envoy:v1.34-latest -c config-tls.yaml
X-Forwarded For 헤더가 필요하지 않고, L4 LB 구성을 선호하는 분들을 위한 레퍼런스 설정 파일입니다.
static_resources:
listeners:
- name: main
address:
socket_address:
address: 0.0.0.0
port_value: 443 # Vault Client Inbound Port
filter_chains:
- filters:
- name: envoy.filters.network.tcp_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
stat_prefix: destination
cluster: hashicorp-vault
clusters:
- name: hashicorp-vault
connect_timeout: 0.5s
type: STRICT_DNS # static
dns_lookup_family: V4_ONLY
lb_policy: round_robin
load_assignment:
cluster_name: hashicorp-vault
endpoints:
- lb_endpoints:
- endpoint:
health_check_config:
port_value: 8200
address:
socket_address:
address: vault01.sjuhwan.lab # replace with vault node address
port_value: 8200 # Vault default port
- endpoint:
health_check_config:
port_value: 8200
address:
socket_address:
address: vault02.sjuhwan.lab # replace with vault node address
port_value: 8200 # Vault default port
- endpoint:
health_check_config:
port_value: 8200
address:
socket_address:
address: vault03.sjuhwan.lab # replace with vault node address
port_value: 8200 # Vault default port
transport_socket_matches: # Enable TLS Connection (healthcheck only)
- name: tls_for_healthcheck
match:
vaultHealthCheck: true
transport_socket: # Enable TLS Connection (healthcheck only)
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: vault.sjuhwan.lab
health_checks:
- timeout: 2s
interval: 1s
unhealthy_threshold: 1
healthy_threshold: 1
tls_options: # Enable TLS Health Check
alpn_protocols: "http/1.1"
http_health_check:
path: "/v1/sys/health"
transport_socket_match_criteria:
vaultHealthCheck: true
common_lb_config:
healthy_panic_threshold: # Disable panic mode (default: 50%)
value: 0.0
admin:
access_log_path: "/dev/null"
address:
socket_address:
address: 0.0.0.0
port_value: 8001
dnf install -y keepalived
cat <<EOF > /opt/envoy/envoycheck.sh
pidof envoy
EOF
sudo chmod 700 /opt/envoy/envoycheck.sh
## Keepalived conf 파일 (개별 노드에 배포)
rm -rf /etc/keepalived/keepalived.conf
vi /etc/keepalived/keepalived.conf
/etc/keepalived/keepalived.conf:
! Configuration File for keepalived
global_defs {
enable_script_security
script_user root
}
vrrp_script chk_envoy {
script "/opt/envoy/envoycheck.sh" #Our custom health check
interval 2 # check every 2 seconds
}
vrrp_instance VI_1 {
state MASTER
interface ens33 #REPLACE WITH YOUR NETWORK INTERFACE
virtual_router_id 51
priority 101 # PRIORITY (Larger is high)
unicast_src_ip 10.11.6.21 # IP Address of this server
unicast_peer {
10.11.6.22
10.11.6.23
}
advert_int 1
authentication {
auth_type PASS
auth_pass MYPASSWORD # REPLACE TO YOUR PASSWORD
}
virtual_ipaddress {
10.11.6.20 #### SHARED IP ADDRESS - VIP
}
track_script {
chk_envoy
}
}
## Keepalived 노드간 통신 허용
firewall-cmd --add-rich-rule='rule protocol value="vrrp" accept' --permanent
firewall-cmd --reload
systemctl start keepalived
systemctl enable keepalived
Keepalived 구성을 성공적으로 완료했다면, 프로덕션 배포를 위한 고가용성 구성이 완료된 것입니다.
이제 Load balancer와 Vault 노드를 셧다운하여 가용성 테스트를 진행하실 수 있습니다.
단, Vault의 경우 재시작 후에는 Unsealing이 필요하다는 것을 기억하십시오.
이것으로, HashiCorp Vault의 프로덕션 배포를 위한 전체 절차를 Step-by-step으로 다루었습니다.
다음 포스팅에서는 AuthN과 ACL Policy를 구성하여 Root Token 없이 Vault에 접근하고, 사용자에게 권한을 부여하는 방법을 자세히 알아볼 것입니다.
Root Token은 최초 관리자 설정에만 사용되어야 하며, 관리자 계정에 권한이 부여된 직후 폐기되어야 한다는 점을 명심하십시오.