ELBでssl転送してnginxでクライアント認証とssl終端してProxyProtocolで送信元IPを取得する

こんにちは。 タイトル長いですがだいたい成功して時がたったので需要があるかはわかりませんが記録しておきます。

お時間あるときにどうぞ。

目的としては、以下のとおりです 外部ELB→nginx(ssl終端かつクライアント認証)→内部ELB→app

つまり、クライアント認証したいけどELBにその機能はないのでtcp443転送してnginxでやるが上位サーバでとれる送信元IPがELBのIPになってしまう為 ELBでProxyProtocolを有効化してnginxでssl終端しつつProxyProtocolをListenして送信元IPをログに出したいという話です。

securityGroupやインスタンス作るなどの部分は省略します。

1.elbをawscliで作成、proxy-protocol設定

参考:

AWS ELBのProxy Protocolを触ってみた Enable or Disable Proxy Protocol Support - Elastic Load Balancing 今更 VPC で 複数の AZ をまたいだ ELB を試す(2)〜 awscli を使って 〜 - ようへいの日々精進 XP

・elb作成
profile=xxxxx
elbname-ext=xxxx-elb
securitygrops="sg-xxxxxxxx sg-yyyyyyyy"
subnets="subnet-xxxxxxxx subnet-yyyyyyyy"
sudo bash -c "aws elb create-load-balancer --load-balancer-name ${elbname-ext} --listeners Protocol=TCP,LoadBalancerPort=443,InstanceProtocol=TCP,InstancePort=443 --subnets ${subnets} --security-groups ${securitygrops} --profile ${profile}"

※外部ELBはTCP443からTCP443に転送するだけで証明書を入れない感じの設定にします

elbname-int=yyyy-elb
securitygrops="sg-xxxxxxxx sg-zzzzzzzz"
sudo bash -c "aws elb create-load-balancer --load-balancer-name ${elbname-int} --listeners Protocol=TCP,LoadBalancerPort=80,InstanceProtocol=TCP,InstancePort=8xxx --subnets ${subnets} --security-groups ${securitygrops} --scheme internal --profile ${profile}"
・helthcheck設定
sudo bash -c "aws elb configure-health-check --load-balancer-name ${elbname-ext} --health-check Target="TCP:443",Interval=30,Timeout=5,UnhealthyThreshold=2,HealthyThreshold=10 --profile ${profile}"

sudo bash -c "aws elb configure-health-check --load-balancer-name ${elbname-int} --health-check Target="TCP:80",Interval=30,Timeout=5,UnhealthyThreshold=2,HealthyThreshold=10 --profile ${profile}"
・インスタンス登録
instances-ext="i-xxxxxxxx i-yyyyyyyyy"
instances-int="i-zzzzzzzz i-aaaaaaaaa"
sudo bash -c "aws elb register-instances-with-load-balancer --load-balancer-name ${elbname-ext} --instances ${instances-ext} --profile ${profile}"
sudo bash -c "aws elb register-instances-with-load-balancer --load-balancer-name ${elbname-int} --instances ${instances-int} --profile ${profile}"
・プロキシプロトコル有効化

ポリシーを作成する

sudo bash -c "aws elb create-load-balancer-policy --load-balancer-name ${elbname-ext} --policy-name EnableProxyProtocol --policy-type-name ProxyProtocolPolicyType --policy-attributes AttributeName=ProxyProtocol,AttributeValue=true --profile ${profile}"
ポリシーを確認する
sudo bash -c "aws elb set-load-balancer-policies-for-backend-server --load-balancer-name ${elbname-ext} --instance-port 443 --policy-names EnableProxyProtocol --profile ${profile}"

無効にする場合は、--policy-names [] という空の指定をすると無効になります

プロキシプロトコルが有効になっていることを確認する

sudo bash -c "aws elb describe-load-balancers --load-balancer-name ${elbname-ext} --profile ${profile}"
"BackendServerDescriptions": [
{
"InstancePort": 443,
"PolicyNames": [
"EnableProxyProtocol"
]
}

2.nginxの設定について

chefでばらまいてますが細かいことは省略しまして、関係する設定内容だけのせます。 proxy-protocol設定反映はreloadではなくrestartする必要があるのでログイン確認の際にnginxのrestartを実施する必要があります。

参考:

Using Proxy Protocol With Nginx | chris lea Module ngx_http_realip_module #355(プロキシのプロトコルサポート) - nginx

実際のnginxの設定ファイル

(成功当時のバージョンは1.7.4-1です)

/etc/nginx/nginx.conf
-----------------------------------------------
log_format main '$proxy_protocol_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'
'$http_x_userid - $http_x_signature - $http_x_sessionkey "$request_body"';
-----------------------------------------------

$remote_addrでなく$proxy_protocol_addrに変更 ※関係あるところだけ抜粋(http_x_sessionkeyなどマスタリングNginxからのコピペですが余計であれば外してください)

/etc/nginx/conf.d/ssl.conf
-----------------------------------------------
server {
# listen 443 default ssl;
listen 443 default ssl proxy_protocol;

server_name <%= node['nginx']['servername1'] %>;
set_real_ip_from <%= node['nginx']['set_real_ip_from'] %>;
real_ip_header proxy_protocol;

ssl_certificate /etc/nginx/<%= node['nginx']['sslcrt1'] %>;
ssl_certificate_key /etc/nginx/<%= node['nginx']['sslkey1'] %>;

ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_session_cache shared:WEB:10m;
ssl_session_timeout 10m;
ssl_ciphers RC4:HIGH:!aNULL:!MD5:@STRENGTH;

## client-auth configuration
ssl_verify_client on;
ssl_verify_depth 3;
ssl_client_certificate /etc/nginx/<%= node['nginx']['clientcrt'] %>;
# ssl_crl /etc/nginx/<%= node['nginx']['clientcrl'] %>;

resolver <%= node['nginx']['resolver'] %> valid=5s;
resolver_timeout 3s;

location / {
proxy_set_header X-FORWARDED-PROTO https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 10s;
proxy_read_timeout 10s;
proxy_pass http://<%= @upstream1 %>:<%= @upstreamport %>$request_uri;

## backend-serverにclient-certを渡す.
# proxy_set_header ssl_client_cert $ssl_client_cert;

# root /usr/share/nginx/html;
# index index.html index.htm;
}
}
-----------------------------------------------

※上記のうちProxyProtocol関係あるディレクティブは、以下のとおりです listen set_real_ip_from real_ip_header

※<%= .. %>で囲んであるところはアトリビュート(変数)になっていて  chefレシピ実行時に設定しておいた値に差し替えられ、実際の値とは異なりますのでお察しください。  @の変数はroleとenvironmentで吸収しきれずdata_bagsから引っ張ってきたりなど。 ※set_real_ip_from は、vpcのセグメントアドレス(例:10.0.0.0/17)を指定してます。  ELBの実際のアドレスを書いとく必要があるけども特定できないし自動で振られるため。 ※ssl_crl(破棄証明リスト)コメントにしてるのは、無期限的な事情によります。

※※ProxyProtocolを解釈できるバージョンは1.5.12以上です。それ以下はconfigtestでエラーでます。 ※※1.4系で--with-proxy-protocolをつけてソースからビルドする必要は全然ありませんし、  1.7ですが特にビルドしたりせず素のパッケージをつかって実現できました。 ※upstreamやめてresolver 追加しました。  nginxの名前解決はマスタリングNginxのP94みたらsetで変数に入れると毎回名前解決し、  それ以外resolverのvaridで指定したttlのタイミングで聞きに行くそうです。  upstreamと*_passで指定した名前を解決するのは最初の一回だけでresolver必要だそう。

nginxの1.7系(mainlineのやつ)を入れるためのyumリポジトリ

中身は以下のような感じです。 chef的にはepelから入るのを防ぐoptions "--disablerepo=epel"をpackageリソースに書く必要があったりなど。

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/mainline/centos/6/$basearch/
gpgcheck=0
enable=1

3.アクセスログの送信元IPを確認する

ログイン
ssh nginx-srv1-dev
ssh nginx-srv2-dev
確認
rpm -qa|grep nginx
sudo service nginx configtest
sudo service nginx restart
sudo service nginx status
sudo netstat -lnptu
chkconfig --list nginx
elbに追加後にInServiceになったらブラウザからアクセスしてログを確認
$ sudo tail -f /var/log/nginx/access.log
119.xxx.xxx.xxx - - [12/Aug/2014:04:39:28 +0000] "GET / HTTP/1.1" 400 648 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36" "-"- - - - - "-"
119.xxx.xxx.xxx - - [12/Aug/2014:04:39:28 +0000] "GET /favicon.ico HTTP/1.1" 400 648 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36" "-"- - - - - "-"

こんな感じに接続元のグローバルIPが出ていたのでProxyProtocol的には成功のようです。 クライアント認証のことはここではこれ以上のことは触れられませんがあしからずご了承ください。

個人的に苦労したのは記事中にリンクのってるchris leaさんのページに書いてあるとおりにやればいいだけというのを理解するまでの紆余曲折と ELBのプロトコルの指定が間違ってたところですかね。合わせて解説してる記事は特に見つからなかったので書いてみました。 nginxユーザー会のイベントで開発者のイゴールさんがSSLつかうなら新しいバージョンを推奨していた意味の一部分を身を以て知ったのでした。 特定デバイスからかぎっ子したい時に役立つかもしれません。

では読んでいただいてありがとうございました。どなたかの役に立ったら嬉しいです。