NGINXのstream設定を追加してTLS offloadとpass throughの両方を機能させる
おうちの環境であれこれサービスを試して遊べるよう、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.conf
をvalues.yaml
などでいじれないようで、client_max_body_size
などをいじったイメージを別途用意し、values.yaml
内、.nginx.image.repository
でそのイメージを指定して導入しています。
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へ渡すようにします。
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/
オフィシャルドキュメント通りです。
まず、stream
のserver
ブロック内にproxy_protocol on;
と、この機能で保持した実際の通信元IPアドレスを渡してよい宛先をset_real_ip_from
で指定する行として追記します。
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;
という行も別途追記しています。
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