TraefikのTCP over TLSを使ってRedisにSNIでアクセスするメモ
TraefikのTCP over TLS機能を使ってRedisにSNI経由でアクセスしてみます。
Traefikは基本的にはHTTP/HTTPSのリバースプロキシですが、TCP Proxyingもサポートしています。TCP Proxyingは、RedisのようなTCPベースのサービスに対しても使えます。 普通にTCPのサービスをtype: LoadBalancerで公開してもいいのですが、TCP over TLS with SNIを使うと次の利点があります。
- TLSのSNIを使って複数のサービスを同じTraefikポートで公開できる (追加でExternal IPを割り当てる必要がない)
- TraefikでTLSの終端を行うので、RedisのようなサービスにTLSを設定する必要がない
- 必要であれば、TraefikをTLS終端にせずTLS Passthroughもできる
RedisをKubernetesにデプロイ
まずは、TCPサービスの例としてRedisをBitnamiのHelmでデプロイします。今回は、永続化を無効にして、認証も無効にしています。
helm upgrade --install redis \
oci://registry-1.docker.io/bitnamicharts/redis \
-n redis \
--create-namespace --wait \
--set master.persistence.enabled=false \
--set replica.persistence.enabled=false \
--set auth.enabled=false
PodとServiceが作成されていることを確認します。
$ kubectl get pod,svc -n redis -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/redis-master-0 1/1 Running 0 3m28s 10.1.173.163 cherry <none> <none>
pod/redis-replicas-0 1/1 Running 0 3m28s 10.1.173.164 cherry <none> <none>
pod/redis-replicas-1 1/1 Running 0 3m 10.1.42.164 banana <none> <none>
pod/redis-replicas-2 1/1 Running 0 2m33s 10.1.247.56 apricot <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/redis-headless ClusterIP None <none> 6379/TCP 3m28s app.kubernetes.io/instance=redis,app.kubernetes.io/name=redis
service/redis-master ClusterIP 10.152.183.167 <none> 6379/TCP 3m28s app.kubernetes.io/component=master,app.kubernetes.io/instance=redis,app.kubernetes.io/name=redis
service/redis-replicas ClusterIP 10.152.183.48 <none> 6379/TCP 3m28s app.kubernetes.io/component=replica,app.kubernetes.io/instance=redis,app.kubernetes.io/name=redis
TLS証明書の作成
cert-managerを使ってTLS証明書を作成します。
cat <<EOF > /tmp/redis-certificate.yaml
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: redis-tls
namespace: redis
spec:
secretName: redis-tls-cert
dnsNames:
- redis.lan.ik.am
issuerRef:
name: letsencrypt # Use your issuer name
kind: ClusterIssuer
---
EOF
kubectl apply -f /tmp/redis-certificate.yaml
証明書が発行されたことを確認します。
$ kubectl get certificate -n redis
NAME READY SECRET AGE
redis-tls True redis-tls-cert 30s
TraefikのIngressRouteTCPを作成
次に、TraefikのIngressRouteTCPを使ってRedisのTCP Proxyingを設定します。
cat <<EOF > /tmp/redis-sni.yaml
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: redis
namespace: redis
spec:
entryPoints:
- websecure # Traefik's 443 port entry point
routes:
- match: HostSNI(\`redis.lan.ik.am\`)
services:
- name: redis-master
port: 6379
tls:
secretName: redis-tls-cert
---
EOF
kubectl apply -f /tmp/redis-sni.yaml
IngressRouteTCPが作成されたことを確認します。
$ kubectl get ingressroutetcp -n redis
NAME AGE
redis 10s
TraefikのExternal IPを確認します。ここでは、traefik namespaceにデプロイされているTraefikのLoadBalancer Serviceを使います。
$ kubectl get svc -n traefik
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
traefik LoadBalancer 10.152.183.23 192.168.11.240 80:31496/TCP,443:31967/TCP 24h
redis.lan.ik.amは192.168.11.240に解決されるようにDNS設定されています。
redis-cliでRedisに接続
次のコマンドで、Redisにアクセスを試みます。
redis-cli -h redis.lan.ik.am -p 443 --tls
次のエラーが出力されます。
Could not connect to Redis at redis.lan.ik.am:443: SSL_connect failed: certificate verify failed
redis-cliはデフォルトではTLSのSNIをサポートしていないため、接続できません。そこで、--sniオプションを使ってSNIを指定します。
redis-cli -h redis.lan.ik.am -p 443 --tls --sni redis.lan.ik.am
今度は接続でき、Key-Valueの読み書きができます。
redis.lan.ik.am:443> set foo 100
OK
redis.lan.ik.am:443> get foo
"100"
次のように-hオプションにTraefikのExternal IPを指定しても同じように接続できます。
redis-cli -h 192.168.11.240 -p 443 --tls --sni redis.lan.ik.am
Spring Boot (Spring Data Redis)アプリケーションからRedisに接続
次にSpring BootアプリケーションからRedisに接続してみます。ここでは、Spring Data Redisを使った簡単なデモアプリケーションを使用します。
次のコマンドでアプリケーションのソースコードを取得し、ビルドします。
git clone https://github.com/making/demo-redis
cd demo-redis
./mvnw clean package -DskipTests
次のコマンドでアプリケーションを起動します。Redisのホスト名とポートを指定して、TLSを有効にします。Spring Data Redis(というよりLettuce?)にredis-cliの--sniオプションに相当するプロパティはなく、
TLSを有効にすれば、接続先のホスト名でSNIを自動的に設定するため、特にSNIの指定は必要ありません。逆に接続先にIPアドレスを指定すると、SNIが設定されずに接続できません。
java -jar target/demo-redis-0.0.1-SNAPSHOT.jar --spring.data.redis.host=redis.lan.ik.am --spring.data.redis.port=443 --spring.data.redis.ssl.enabled=true
次のコマンドで、アプリケーションが機能していることを確認します。
$ curl "http://localhost:8080?key=foo"
100
$ curl "http://localhost:8080?key=foo" -H "Content-Type: text/plain" -d Hello
$ curl "http://localhost:8080?key=foo"
Hello
TLS Passthroughを使う場合
TraefikをTLS終端にせず、TLS PassthroughでRedis側でTLSを処理したい場合は、次のように設定します。
cat <<EOF > /tmp/redis-sni-passthrough.yaml
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: redis
namespace: redis
spec:
entryPoints:
- websecure # Traefik's 443 port entry point
routes:
- match: HostSNI(\`redis.lan.ik.am\`)
services:
- name: redis-master
port: 6379
tls:
passthrough: true # Enable TLS passthrough
---
EOF
kubectl apply -f /tmp/redis-sni-passthrough.yaml
この場合、Redis側でTLSの設定が必要になります。
TraefikのTCP over TLSを使うことで、RedisのようなTCPサービスに対してもSNIを利用したTLS接続が可能になります。 これにより、複数のサービスを同じポートで公開できるため、リソースの効率的な利用が可能になります。