🤹‍♂️

NGINXのstream設定を追加してTLS offloadとpass throughの両方を機能させる

2023/12/13に公開

おうちの環境であれこれサービスを試して遊べるよう、DockerやKubernetesなどを利用しています。

今回、NGINXでstreamの設定を追加して、TLS offloadしているリバースプロキシ及びKubernetesクラスタのGatewayへのTLS pass throughの両方を動作させるようにしました、その紹介ポストです。

なお2023年12月に公開した時点での内容だと通信元IPアドレスの扱いに難がありましたが、その辺りも直す方法を2024年2月に追記しています。

セットアップの紹介

Gateway

まずKubernetesのGatewayの紹介です。コントローラとしてNGINX Gateway Fabricを導入しています。

Gatewayのマニフェストです。.spec.listens[]に関しては省略して443のみにしております。

apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
  name: my-gateway
  namespace: gateway
spec:
  gatewayClassName: nginx
  listeners:
  - name: https
    port: 443
    protocol: HTTPS
    tls:
      mode: Terminate
      certificateRefs:
        - kind: Secret
          name: tls-secret
          namespace: certificate
    allowedRoutes:
      namespaces:
        from: Selector
        selector:
          matchLabels:
            gateway-enabled: yes

TLS証明書はcertificate namespaceに格納しており、参照許可マニフェストがこちらです。

apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-gtw
  namespace: certificate
spec:
  to:
  - group: ""
    kind: Secret
    name: tls-secret
  from:
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: gateway

あとはGatewayを利用させたいサービスはgateway-enabled=yesとnamespaceにラベル付けして、例えば以下のようなHTTPRouteマニフェストを用意しています。

apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: service_name_here
  namespace: {whichever_ns_with_gateway-enabled=yes_label}
spec:
  parentRefs:
  - name: my-gateway
    sectionName: https
    namespace: gateway
  hostnames:
  - "hostname.yourdomainname.here"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: service_name_here
      port: 80

なおGateway ControllerであるNGFはまだnginx.confvalues.yamlなどでいじれないようで、client_max_body_sizeなどをいじったイメージを別途用意し、values.yaml内、.nginx.image.repositoryでそのイメージを指定して導入しています。

https://github.com/nginxinc/nginx-gateway-fabric/issues/1245#issuecomment-1817240607

https://github.com/nginxinc/nginx-gateway-fabric/blob/v1.0.0/docs/building-the-images.md

NGINXリバースプロキシ

Dockerで走らせているリバースプロキシサーバのセットアップの紹介も、今回紹介したいところに関連したところに絞ります。

ディレクトリは以下のような感じで、docker composeで走らせています。

.
 |-docker-compose.yml
 |-conf
 | |-cert
 | | |-privkey.pem  # tls cert, private key
 | | |-ssl-base.conf  # common ssl/tls conf
 | | |-fullchain.pem  # tls cert, full chain
 | | |-ssl-dhparams.pem  # dhparams
 | |-nginx.conf  # basic nginx.conf
 | |-nginx
 | | |-svc1.conf  # http server conf to be included by the base nginx.conf
 | | |-svc2.conf

docker-compose.ymlファイルの内容です。イメージのタグなども省略しています。

services:
  nginx:
    image: nginx
    container_name: nginx
    ports:
      - "443:443"
    volumes:
      - ./conf/nginx/:/etc/nginx/conf.d
      - type: bind
        source: ./conf/cert
        target: /etc/nginx/cert
        read_only: true
      - type: bind
        source: ./conf/nginx.conf
        target: /etc/nginx/nginx.conf
        read_only: true

./conf/nginx.conf
の内容ですが、本題では少し追記しますが、デフォルトのままかと思います。もし http { include /etc/nginx/conf.d/*.conf;}がなければ追記必要です。

SSL/TLS関連の設定は./conf/cert/ssl-base.confにまとめていて、./conf/nginx/svc1.confなどTLS offloadするリバースプロキシサーバ設定に読み込ませています。

# cert
ssl_certificate /etc/nginx/cert/fullchain.pem;
ssl_certificate_key /etc/nginx/cert/privkey.pem;

# dhparam
ssl_dhparam /etc/nginx/cert/ssl-dhparams.pem;

# options
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";

./conf/nginx/svc1.confはこんな感じです。

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

upstream svc1 {
    server 10.2.3.4:8080;
    keepalive 90;
}


server {
    listen 443 ssl;
    http2 on;
    server_name svc1.yourdomainname.here;
    # upload size
    client_max_body_size 5M;
    # ssl
    include /etc/nginx/cert/ssl-base.conf;

    location / {
        proxy_http_version 1.1;
        proxy_pass http://svc1;
        proxy_set_header Host $host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

NGINXのstream設定

Kubernetes上のGatewayもDockerで動かしているNGINXも、443でlistenし、TLS offloadし、SNIに対応して各サービスへ通信を流すというセットアップになっています。

本題ですが、NGINXのstreamという設定を追加して、Kubernetesクラスタ内のサービス宛の通信もNGINXが受けて、TLS pass throughでGatewayへ渡すようにします。

https://docs.nginx.com/nginx/admin-guide/load-balancer/tcp-udp-load-balancer/#configuring-reverse-proxy

streamディレクティブはhttpディレクティブと同レベルに書くということなので、./conf/nginx.confを書き換えます。

ポイントは一点です。streamのserverにlisten 443;を設定し、既存のhttpのserverすべてはlisten 8443;に更新することです。

stream { server { listen 443; } }が既存のリバースプロキシサーバのhttp { server { listen 443 ssl; } }と競合してしまうので、nginx -tのチェックは通るのですが立ち上げてもすぐクラッシュします。

streamの443にSNIに応じてGatewayに渡す、自分の既存のリバースプロキシサーバに渡すという挙動を指定しています。

stream {
  # map sni and backend
  map $ssl_preread_server_name $name {
    hostnames;
    k8ssvc.yourdomainname.here k8s_svc;
    *.yourdomainname.here reverse_proxy;
  }
  # each backend/upstream
  upstream k8s_svc {
    server 192.168.1.54:443 max_fails=3 fail_timeout=30s;
  }
  upstream reverse_proxy {
    server 127.0.0.1:8443 max_fails=3 fail_timeout=30s;
  }
  # listen
  server {
    listen 443;
    proxy_pass $name;
    ssl_preread on;
  }
}

上の例では個別にk8ssvc.yourdomainname.here分のマッピングを書いていますが、map $ssl_preread_server_name $name {}内ではワイルドカードもregexも使えるので、いっそKubernetesクラスタ側は別ドメインなどを使わせればワイルドカードでまとめて、宛先サービス名が増えるたびに設定を更新する必要はなくなります。

通信元IPアドレスの保持

2024年2月に追記しています。

以上の設定変更でめでたくstreamでそのまま通信を流すTLS pass-throughもhttpでTLS offloadして通信を流すリバースプロキシのどちらも動かせるようになったのは良いのですが、後ろのサービスから見える通信元IPアドレスがすべて127.0.0.1になってしまいました。

AutheliaというMFA機能を提供してくれるサービスを使い、単純にIPアドレスベースでアクセス制御することで、おうち内からは楽々無認証アクセス、外部からのアクセスだけにMFA要求するということをしていましたが、これが台無しになりました。

この点もなんとか直していたので追記としてその部分を紹介します。

PROXYプロトコル有効化

https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/

オフィシャルドキュメント通りです。

まず、streamserverブロック内にproxy_protocol on;と、この機能で保持した実際の通信元IPアドレスを渡してよい宛先をset_real_ip_fromで指定する行として追記します。

nginx.conf
stream {
  # map sni and backend
  map $ssl_preread_server_name $name {
    hostnames;
    k8ssvc.yourdomainname.here k8s_svc;
    *.yourdomainname.here reverse_proxy;
  }
  # each backend/upstream
  upstream k8s_svc {
    server 192.168.1.54:443 max_fails=3 fail_timeout=30s;
  }
  upstream reverse_proxy {
    server 127.0.0.1:8443 max_fails=3 fail_timeout=30s;
  }
  # listen
  server {
    listen 443;
    proxy_pass $name;
    ssl_preread on;
    proxy_protocol on;
    set_real_ip_from 10.0.0.0/8;
    set_real_ip_from 172.16.0.0/12;
    set_real_ip_from 192.168.0.0/16;
    set_real_ip_from 127.0.0.0/8;
  }
}

そしてhttpの方では、listen行でproxy_protocolを追加、そしてreal_ip_header proxy_protocol;という行も別途追記しています。

svc1.conf
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

upstream svc1 {
    server 10.2.3.4:8080;
    keepalive 90;
}


server {
    listen 8443 ssl proxy_protocol;
    http2 on;
    server_name svc1.yourdomainname.here;

    # real ip
    real_ip_header proxy_protocol;

    # upload size
    client_max_body_size 5M;
    # ssl
    include /etc/nginx/cert/ssl-base.conf;

    location / {
        proxy_http_version 1.1;
        proxy_pass http://svc1;
        proxy_set_header Host $host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

こうするとhttp.serverの方へも$proxy_protocol_addrという変数で実際の通信元IPアドレスを渡すことができます。例えば私の場合は、Authelia用のNGINX confファイルで元々$remote_addrとして指定していた部分を$proxy_protocol_addrへ変更することで、元通りIPアドレスでの単純なアクセス制御ができるようになりました。

    # proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Real-IP $proxy_protocol_addr;

おわりに

以上です!

おうちであれこれ環境を作って遊ぼうとしたとき、最初はDockerから手を付け始めたのですが、メインのDNSサーバや、ウェブサービスへアクセスを経由させるためのリバースプロキシサーバをNGINXで構築しました。ワイルドカードTLS証明書を用意し、TLS offloadさせています。

その後、Kubernetesクラスタも構築し、クラスタ内で走っているウェブサービスへのアクセスを用意するためにKubernetes Gateway APIを利用するNGINX Gateway Fabricも導入しました。こちらもGatewayにはワイルドカードTLS証明書を用意し、TLS offloadさせています。

どちらもおうちのLAN上では自在にアクセスできて便利なのですが、ISPに借りているグローバルIPアドレスが一つだけであるため、外部からの:443通信はリバースプロキシへ流すようにしており、Kubernetesクラスタ内のサービスへはアクセスできない状態でした。というわけで今回はこの問題を解消した際のやり方を紹介したいというポストでした。

Discussion