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.am192.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接続が可能になります。 これにより、複数のサービスを同じポートで公開できるため、リソースの効率的な利用が可能になります。