[브라이튼의 후방 수적 우위가 가져다준 효과는?]
이 글은 TLS 에 대한 기초적인 개념이 필요합니다.
최소한 HTTPS 가 어떻게 동작하며, CA, CSR, tls.cert 등과 같은 것이 무엇인지 알아야 합니다.
미니 PC 에서 쿠버네티스 클러스터를 구성하여 이것저것 해보고 있는데, 늘 하나 마음에 걸리는 것이 있었습니다.
바로 도메인과 TLS 입니다!
도메인 같은 경우 맥 /etc/host 에 추가해서 사용하고 있었고, TLS 가 적용되지 않다보니 늘 주의 요함을 달고 있었죠.
AWS ALB 를 사용하면 Route53 과 ACM 을 통해서 TLS 적용하기 쉽지만, AWS 없이 TLS 를 적용하려면 결국 TLS 를 구매해야 합니다. TLS 는 보통 유료인데, Let's Ecrypt 는 무료로 TLS 을 제공해주기 때문에 이번 글에서는 로컬 환경에서 Let's Ecrypt 와 Istio 를 이용해서 무료로 TLS 를 적용해보고자 합니다.
들어가기 앞서 준비할 것이 있습니다. 바로 TLS 를 적용하고자 하는 도메인입니다.
저 같은 경우 CloudFlare 에서 도메인을 구매하였고, 다음과 같이 DNS 레코드를 구성했습니다.
httpbin.kingbj0429.uk 와 같이 서브도메인을 사용할 것이기 때문에 와일드카드를 이용한 레코드를 추가했습니다.
그럼 본격적으로 시작해보죠!
렛츠두더코드~
먼저 Cert Manager 에 대해 조금은 알아보죠!
쿠버네티스 환경에는 무수히 많은 파드가 있습니다. 민감한 데이터를 주고 받는 파드라면 당연히 TLS 을 통해 안전한 통신을 해주어야 합니다. 하나의 파드에 TLS 를 적용하는 건 살짝 귀찮아도 어렵지 않아요. TLS 가 만료가 되도, 파드 하나에 대해서만 갱신해주면 되기 때문에 문제가 없어요.
하지만 파드가 100개라면 어떨까요? 갱신하는 것만으로도 벌써 일주일이 다 갈 겁니다.
그래서 TLS 인증서를 편리하게 관리할 수 있도록 도와주는 것이 바로 Cert Manager 입니다.
Issuer 를 통해 언제든지 Certificate 를 만들 수 있고, 갱신 또한 알아서 다 해줍니다.
Cert Manager 를 사용해도 파드 간 통신에 TLS 를 적용하는 건 상당히 귀찮습니다.
그래서 Istio 를 사용하게 되면 보통 mTLS 를 통해 파드가 통신을 암호화 해줍니다.
mTLS 에 대해 궁금하다면 여기 참고!
근데 여기서 문제는 Issuer 같은 경우 보통 SelfSigned 를 하기 때문에 일반 브라우저에서 접근하고자 하려면 주의 요함이 떠요.
왜냐 믿을 수 없는 ca.cert 이기 때문이죠.
그래서 우리는 SelfSigned 가 아닌, Certificate Authority 가 제공해주는 ca.cert 가 필요하고, ca.cert 를 통해 발급받은 tls.cert 가 필요합니다.
Let's Ecrypt 는 Certificate Authority 이기 때문에 여기서 검증 받은 tls.cert 를 사용하면 주의 요함을 없앨 수 있습니다.
SelfSigned 를 통해 검증 받은 tls.cert 도 주의 요함을 없앨 수 있는데, 바로 브라우저에 SelfSigned 된 ca.cert 를 추가하는 것이죠.
맥 같은 경우 키 체인에 추가해주면 됩니다.
하지만 스코프가 완전히 로컬이기 때문에 다른 사람이 결국 접근하려고 하면 주의 요함이 발생하죠.
요약해보자면 Let's Ecrypt CA 가 발급해준 tls.cert 를 사용하면 안전한 https 통신이 가능합니다.
Cert Manager 는 이러한 tls.cert 를 사용자 대신 생성해주고, 자동 갱신 등 다앙햔 기능을 제공해줍니다.
그럼 이제 진짜 실습으로 들어가보죠!
만들고자 하는 아키텍처는 아래와 같아요.
도메인 소유권을 확인하기 위해서 ACME(Automated Certificate Management Environment) 프로토콜을 사용하게 되는데, 공식 문서에서 각각 도메인에 맞는 것을 사용하면 됩니다.
저는 CloudFlare 를 통해 도메인을 구매했기 때문에 도메인 소유권을 확인하기 위해서는 cloudflare 의 api 가 필요합니다.
다양한 ACME 를 제공하니 본인 환경에 맞게 적용하시면 될 거 같아요.
공식 문서에 나와있는 대로 저는 API Token 을 생성했고, Secret 과 Issuer 를 생성했습니다.
여기서 정말 중요한 포인트가 있습니다!!!
공식 문서를 보면 Secret 리소스에서 data 가 아닌 stringData 를 사용하고 있는데 data 를 사용해줍시다.
apiVersion: v1
data:
api-token: <base64 encoded api token>
kind: Secret
metadata:
name: cloudflare-api-token-secret
namespace: istio-system
type: Opaque
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt-dns01-prod-issuer
namespace: istio-system
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: kingbj0429@gmail.com
privateKeySecretRef:
name: letsencrypt-dns01-prod-key-pair
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token-secret
key: api-token
ACME 프로토콜은 도메인의 소유자를 확인하고 인증서를 발급 가능케 해주는 프로토콜 입니다.
크게 종류는 DNS-01 과 HTTP-01 이 있죠.
DNS-01 은 DNS 레코드를 통해 소유권을 확인하고,
HTTP-01 은 HTTP 요청을 통해 소유권을 확인하게 됩니다.
API Token 권한은 문서 그대로 진행하시면 되는데 혹시나 해서 첨부합니다.
그럼 이제 생성한 Issuer 를 통해 Certificate 를 생성해보죠.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: kingbj0429-uk-cert
namespace: istio-system # gateway 에 적용하기 위해선 반드시 istio 배포한 네임스페이스로!!
spec:
isCA: false
secretName: kingbj0429-uk-key-pair
commonName: kingbj0429.uk
dnsNames:
- kingbj0429.uk
- httpbin.kingbj0429.uk
duration: 2160h # 90d
renewBefore: 360h # 15d
privateKey:
algorithm: RSA
encoding: PKCS1
size: 4096
issuerRef:
name: letsencrypt-dns01-prod-issuer
kind: Issuer
group: cert-manager.io
Istio Gateway 에 적용하려면 반드시 Istio 를 배포한 네임스페이스에 Certificate 를 생성해주어야 합니다!!
배포하게 되면 이제 본격적으로 Cert Manager 가 해당 도메인에 TLS 를 적용하기 위한 인증서를 발급 해주는 메커니즘이 동작하게 됩니다.
아래와 같이 다양한 CRD 가 생성됩니다.
$ k get orders.acme.cert-manager.io -n istio-system
#NAME STATE AGE
#kingbj0429-uk-cert-wr6b9-1120062372 pending 81s
$ k get challenges.acme.cert-manager.io -n istio-system
#NAME STATE DOMAIN AGE
#kingbj0429-uk-cert-wr6b9-1120062372-1523595522 pending httpbin.kingbj0429.uk 81s
#kingbj0429-uk-cert-wr6b9-1120062372-2843859647 pending kingbj0429.uk 81s
$ k get certificaterequests.cert-manager.io -n istio-system
#NAME APPROVED DENIED READY ISSUER REQUESTOR AGE
#kingbj0429-uk-cert-fzhkf True False letsencrypt-dns01-prod-issuer system:serviceaccount:key-manager:cert-manager 83s
$ k get certificate -n istio-system
#NAME READY SECRET AGE
#kingbj0429-uk-cert False kingbj0429-uk-key-pair 7m
Certificate 리소스에서 .spec.dnsNames[] 에 도메인을 2개 명시했기 때문에 challenge 가 2개인걸 확인할 수 있습니다.
결국 Certificate 를 받기 위한 과정이죠.
하나씩 간략하게 설명하면,
- CertificateRequest: TLS 인증서를 요청하고, 이 요청은 특정 Issuer 또는 ClusterIssuer와 관련이 있음
- Order: CertificateRequest를 기반으로 CA에게 인증서를 요청하는 주문을 나타냄
- Challenge: 인증서를 발급하기 위해 CA가 인증서를 발급할 수 있는지 확인하는 방법을 나타냄
메커니즘 순서를 아래와 같죠.
(만약 인증서 라이프사이클에 대해 자세히 알고 싶다면 여기를 참고!)
- CertificateRequest 를 생성하면, Cert Manager는 해당 요청에 대한 Order를 생성
- Order가 생성되면, Cert Manager는 해당 Order에 대한 Challenge를 생성
- Challenge가 생성되면, Cert Manager는 해당 Challenge를 완료하기 위한 방법을 제공합니다. DNS-01 or HTTP-01 방식의 ACME 프로토콜을 통해 도메인 소유권을 확인
- Challenge가 완료되면, Cert Manager는 도메인 소유를 확인하고 Order를 완료하며, 인증서를 발급
한 줄로 요약하면 Issuer.yaml 에 작성한 acme 코드를 통해 도메인 소유권을 확인하고, 소유권이 확인되면 인증서를 발급해줍니다.
한 2분 정도가 되면 이제 위에 리소스들이 모두 Valid 또 True 가 됩니다. 그럼 성공적으로 Let's Encrypt 를 통해 TLS 를 발급 받게 된 것이죠.
$ k get issuers.cert-manager.io -n istio-system
#NAME READY AGE
#letsencrypt-dns01-prod-issuer True 14m
$ k get orders.acme.cert-manager.io -n istio-system
#NAME STATE AGE
#kingbj0429-uk-cert-wr6b9-1120062372 valid 81s
$ k get challenges.acme.cert-manager.io -n istio-system
#NAME STATE DOMAIN AGE
#kingbj0429-uk-cert-wr6b9-1120062372-1523595522 valid httpbin.kingbj0429.uk 81s
#kingbj0429-uk-cert-wr6b9-1120062372-2843859647 valid kingbj0429.uk 81s 81s
$ k get certificaterequests.cert-manager.io -n istio-system
#NAME APPROVED DENIED READY ISSUER REQUESTOR AGE
#kingbj0429-uk-cert-fzhkf True False letsencrypt-dns01-prod-issuer system:serviceaccount:key-manager:cert-manager 83s
$ k get certificate -n istio-system
#NAME READY SECRET AGE
#kingbj0429-uk-cert True kingbj0429-uk-key-pair 7m
challenges.acme.cert-manager.io 리소스 같은 경우는 valid 상태가 되면 자동으로 삭제되기 때문에
-w 옵션을 주어 상태를 확인할 수 있습니다.
한번 발급받은 TLS 를 확인해봅시다. TLS 은 시크릿으로 배포됩니다.
$ k describe secrets -n istio-system kingbj0429-uk-key-pair
#Name: kingbj0429-uk-key-pair
#Namespace: istio-system
#Labels: controller.cert-manager.io/fao=true
#Annotations: cert-manager.io/alt-names: httpbin.kingbj0429.uk,kingbj0429.uk
# cert-manager.io/certificate-name: kingbj0429-uk-cert
# cert-manager.io/common-name: kingbj0429.uk
# cert-manager.io/ip-sans:
# cert-manager.io/issuer-group: cert-manager.io
# cert-manager.io/issuer-kind: Issuer
# cert-manager.io/issuer-name: letsencrypt-dns01-prod-issuer
# cert-manager.io/uri-sans:
#
#Type: kubernetes.io/tls
#
#Data
#====
#tls.key: 3243 bytes
#tls.crt: 5883 bytes
시크릿에 의해 생성된 tls.crt 를 그럼 이제 디코딩을 해보죠.
echo "..." | base64 -d -o tls.cert
-----BEGIN CERTIFICATE-----
MIIF/DCCBOSgAwIBAgISBGYsMjD73J679u2U7ax5UXKVMA0GCSqGSIb3DQEBCwUA
...
8A/W4lHPy1UeCaHIs8j6QQwYp9JNXHle+yKP5obPb3f8GeCw2yAp91gffeovBFxn
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw
...
nLRbwHOoq7hHwg==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/
...
Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5
-----END CERTIFICATE-----
그럼 이제 디코딩된 값으로 certificate 내용을 확인해보죠.
$ openssl x509 -in ./tls.cert -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
04:66:...
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, O = Let's Encrypt, CN = R3
Validity
Not Before: Sep 7 11:44:12 2023 GMT
Not After : Dec 6 11:44:11 2023 GMT
Subject: CN = kingbj0429.uk
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Modulus:
00:d5:96:ea:78:68:68:ae:88:47:a5:73:24:df:c4:
...
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Key Identifier:
BA:DD:...
X509v3 Authority Key Identifier:
14:2E:...
Authority Information Access:
OCSP - URI:http://r3.o.lencr.org
CA Issuers - URI:http://r3.i.lencr.org/
X509v3 Subject Alternative Name:
DNS:httpbin.kingbj0429.uk, DNS:kingbj0429.uk
X509v3 Certificate Policies:
Policy: 2.23.140.1.2.1
CT Precertificate SCTs:
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : 7A:32:8C:54:D8:B7:2D:B6:20:EA:38:E0:52:1E:E9:84:
...
Timestamp : Sep 7 12:44:12.629 2023 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:44:02:20:4A:CB:24:7A:13:0D:A9:20:3C:80:68:9B:
...
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : AD:F7:BE:FA:7C:FF:10:C8:8B:9D:3D:9C:1E:3E:18:6A:
...
Timestamp : Sep 7 12:44:12.676 2023 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:44:02:20:54:A6:7F:D0:4E:72:79:CA:01:EE:B2:FB:
...
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
88:3b:98:ea:0a:bf:3c:5f:12:a1:a5:f2:8f:bc:36:cd:fe:9e:
...
몇가지 핵심 정보를 확인해보면,
O=Let's Encrypt 이고, Subject: CN = kingbj0429.uk 입니다. 또한 CA:FALSE 입니다.
간략하게 그림으로 표현하면 아래와 같이 될 수 있을거 같아요.
만약 인증서 라이프사이클에 대해 자세히 알고 싶다면 여기를 참고!
Let's Encrypt 를 통해 인증서를 발급받았으니 이제 실제로 적용해봅시다.
httpbin 을 배포하고자 합니다.
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: kingbj0429-test-gateway
spec:
selector:
istio: gateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "httpbin.kingbj0429.uk"
- "kingbj0429.uk"
- port:
number: 443
name: https
protocol: HTTPS
tls:
mode: SIMPLE
credentialName: kingbj0429-uk-key-pair
hosts:
- "httpbin.kingbj0429.uk"
- "kingbj0429.uk"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: kingbj0429-test-virtual-service
spec:
hosts:
- "httpbin.kingbj0429.uk"
- "kingbj0429.uk"
gateways:
- kingbj0429-test-gateway
http:
- match:
- uri:
prefix: /
route:
- destination:
host: httpbin
port:
number: 8000
---
apiVersion: v1
kind: Service
metadata:
name: httpbin
labels:
app: httpbin
service: httpbin
spec:
ports:
- name: http
port: 8000
targetPort: 80
selector:
app: httpbin
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpbin
spec:
replicas: 1
selector:
matchLabels:
app: httpbin
version: v1
template:
metadata:
labels:
app: httpbin
version: v1
spec:
serviceAccountName: httpbin
containers:
- image: docker.io/kong/httpbin
imagePullPolicy: IfNotPresent
name: httpbin
ports:
- containerPort: 80
성공적으로 TLS 가 적용이 됐습니다.
포트가 32003 인 이유는 istio ingressgateway 의 https 포트가 노드의 32003 과 맵핑되기 때문입니다.
인증서 뷰어를 확인했을 때도 문제는 없어보입니다.
생각보다 글이 너무 길어졌네요..
Cert Manager, TLS, ACME, Domain, DNS, Istio Gateway 등 다양한 개념들이 있다니 생각보다 어려웠던 실습이였습니다.
그래도 무사히 성공!
다음 글에서는 AWS Route53 에 적용해볼 예정입니다.
그럼 오늘은 여기까지!