mruby_nginx_module を使用して,Riak CS や Amazon S3 のオブジェクトをブラウザから取得する

Riak CS や Amazon S3 に置いたファイルを非公開にした状態で,Nginx を介した場合にのみ,アクセス元のユーザは Access Key ID も Secret Access Key も使用せずに,非公開設定のファイルを取得します.

構成

Nginx 側で,Amazon S3 API の Authorization ヘッダを計算/追加して,プロキシします.

f:id:Hexa:20131221152615p:plain

環境準備

# git clone https://github.com/cubicdaiya/mruby_nginx_module.git
# cd mruby_nginx_module
# git submodule update --init
# cd mruby
# make
# cd
# git clone https://github.com/simpl/ngx_devel_kit.git
# curl -O http://nginx.org/download/nginx-1.4.4.tar.gz
# tar zxvf nginx-1.4.4.tar.gz
# cd nginx-1.4.4
# ./configure --prefix=/opt/nginx/1.4.4 --add-module=../mruby_nginx_module --add-module=../ngx_devel_kit --with-http_ssl_module
# make && make install
nginx.conf の設定
        location / {
                if ( $request_method != GET ) {  return 400; }
                set $aws_access_key_id "aws_access_key_id";
                set $aws_secret_access_key "aws_secret_access_key";
                set $bucket_name "bucket";
                set $base_url "example.com";
                mruby_set_code $date "Nginx::Time.http_time(Nginx::Time.time)";
                mruby_set_code $authorization '

                        content_md5 = ""
                        content_type = ""

                        request = Nginx::Request.new
                        var = Nginx::Var.new
                        string_to_sign = request.method + "\n" +
                                         content_md5 + "\n" +
                                         content_type + "\n" +
                                         var.date + "\n" +
                                         "/" + var.bucket_name + request.uri
                        hmac_sha1 = Nginx::Digest.hmac_sha1(string_to_sign, var.aws_secret_access_key)
                        "AWS " + var.aws_access_key_id + ":" + Nginx::Base64.encode(hmac_sha1)
                ';

                proxy_set_header Host $bucket_name.$base_url;
                proxy_set_header Date $date;
                proxy_set_header Authorization $authorization;
                proxy_pass http://$bucket_name.$base_url:80$uri;
        }

elasticsearch + Kibana でログの可視化

動かしただけで,理解していないので,作業した内容だけをメモしておきます.

ab で Apache に負荷をかけて,その access.log のグラフを表示します.

elasticsearch の準備/起動

# aptitude -y install openjdk-7-jdk
# curl -O https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-0.90.7.tar.gz
# tar zxvf elasticsearch-0.90.7.tar.gz
# cd elasticsearch-0.90.7
# ./bin/elasticsearch -f

Kibana3 の準備

# aptitude install apache2
# cd /var/www
# curl -O https://download.elasticsearch.org/kibana/kibana/kibana-3.0.0milestone4.tar.gz
# tar zxvf kibana-3.0.0milestone4.tar.gz
# mv kibana-3.0.0milestone4 kibana

logstash を使用する場合

# mkdir logstash
# cd logstash
# curl -O https://download.elasticsearch.org/logstash/logstash/logstash-1.2.2-flatjar.jar
# cat <<__EOT__ >logstash.conf
> input {
>   file {
>     type => "apache"
>     path => "/var/log/apache2/access.log"
>   }
> }
>
> filter {
>   grok {
>     type => "apache"
>     pattern => "%{COMBINEDAPACHELOG}"
>   }
> }
>
> output {
>   elasticsearch {
>     embedded => true
>   }
> }
> __EOT__
# java -jar logstash-1.2.2-flatjar.jar agent -f logstash.conf -- web

fluentd を使用する場合

# gem install fluentd
# gem install fluent-plugin-elasticsearch
# cat <<__EOT__>fluent.conf
> <source>
>   type tail
>   format apache
>   path /var/log/apache2/access.log
>   pos_file /var/log/apache2/access.pos
>   tag apache2-access
> </source>
>
> <match apache2-access>
>   type elasticsearch
>   host 192.168.1.2
>   port 9200
>   logstash_format true
> </match>
> __EOT__
# fluentd -c fluent.conf

グラフの確認

f:id:Hexa:20131204010431p:plain

下記のように ab で適当に負荷をかけつつ,ブラウザで http://192.168.1.2/kibana/index.html#/dashboard/file/logstash.json にアクセスして、グラフを確認します.

# ab -c 10 -n 1000000  http://192.168.1.2/index.html

Ansible のメモ

Ansible 使用時に手間取った点がいくつかあったので書いておきます.

ディストリビューション毎に処理を分ける

ディストリビューションによって,yum と apt-get 等を使い分けたい場合は,
centos.yml や ubuntu.yml 等を作成して,yum や apt-get 等の処理を書いておきます.

main.yml

---
- include: ubuntu.yml
  when: ansible_distribution == "Ubuntu"

ubuntu.yml

---
- shell: apt-get -y update
- shell: apt-get -y install aptitude
- shell: aptitude -y install {{ item }}
  with_items:
    - make
    - gcc
    - curl
    - git

環境変数を指定する

環境変数を指定したい場合には environment: を指定します.

- shell: make
  environment:
    PATH: /opt/local/bin:/usr/bin:/usr/local/bin

PATH の場合は,追加だけしたいので,次のように指定した方が良いかもしれません.

- shell: make
  environment:
    PATH: "/opt/local/bin:{{ ansible_env.PATH }}"

ansible_facts で取得できない対象サーバの情報を指定したい場合には,stdout の値を使用することもできます.

- shell: echo $PATH
  register: path

- shell: echo $PATH
  environment:
    PATH: "/opt/local/bin:{{ path.stdout }}"

git clone

Ansible には git モジュールがあるため,簡単に git リポジトリを clone できますが,
GitHub 等から ssh で clone する場合にはいくつか注意点があります.

  • ホストキーのチェックによる処理の停止
  • 公開鍵認証に使用する鍵の管理
ホストキーのチェックによる処理の停止

仮想環境等で、必要になった場合にのみインスタンスを作成するような場合には,
~/.ssh/known_hosts に Git/GitHub のサーバにホスト鍵は追加されていないことがよくあります.

このような環境で,StrictHostKeyChecking のデフォルト値が ask の場合には,git clone を実行するとホスト鍵の確認が入るため,Git/GitHub のサーバへのアクセスで処理が止まります.

これを回避するために,.ssh/config や /etc/ssh/ssh_config に StrictHostKeyChecking no を設定しておきます.


git モジュール を使用する前に,Git/GitHub のサーバへのアクセスする際の設定に StrictHostKeyChecking no を記載した ssh_config を, copy モジュール でアップロードしておきます.

- copy: src=ssh_config dest=/etc/ssh/ssh_confg
- git: repo=... dest=/home/user/...
公開鍵認証に使用する鍵の管理

自分以外の誰かがアクセスできるサーバ上には、秘密鍵は置きたくないです.
そのため,ssh でログインしてリポジトリを clone する場合には ForwardAgent をすることが多いと思います.

ansible を使用する場合でも,通常の ssh の場合と同じように ForwardAgent を使用して,サーバ上に秘密鍵を置かずに,リポジトリを取得します.(ansible.cfg の設定以外は ssh でログインする場合と同じような手順です)

1. ansible.cfg の準備

$ cat ansible.cfg
[ssh_connection]
ssh_args = -o ForwardAgent=yes

2. ssh-agent と ssh-add

$ ssh-aget -t 3600 bash
$ ssh-add ~/.ssh/id_rsa

3. ansible-playbook の実行

$ ansible-playbook -i hosts playbook.yml

Riak CS の SSL を有効にして AWS SDK for Ruby でファイルをアップロードする

手順

1. 証明書を作成する
2. bucket を決めて,DNS にドメインを登録する
3. Riak CS の SSL 機能を有効にして起動させる
4. curl を使用して,admin ユーザのアカウントを作成する
5. AWS SDK for Ruby をインストールする
6. ファイルをアップロードする
7. boto で確認してみる

環境

  • Riak: 1.4.1
  • Stanchion: 1.4.0
  • Riak CS: 1.4.0
  • Ruby: 2.0.0-p195
  • aws-sdk: 1.15.0

1. 証明書を作成する

Amazon S3 と同様で,2 つの URL を使用します.

そのため,サーバ証明書の X509v3 Subject Alternative Name に example.com と,<bucketname>.example.com を指定しておきます.
後で bucket を追加する場合を考えて,Amazon S3サーバ証明書と同様に * を使用して,下記のように指定しておくと便利です.

            X509v3 Subject Alternative Name:
                DNS:*.example.com, DNS:example.com

2. bucket を決めて,DNS にドメインを登録する

HTTP の場合と異なり,SSL を使用する場合には,クライアントの HTTP プロキシの指定は使用できません.
HTTPS の場合,クライアントは,はじめに HTTP プロキシサーバに向かって CONNECT メソッドのリクエストを送り,トンネル接続の確立を要求するためです.

そのため,リクエストのホスト名の名前解決が必要になります.

例えば,bucket1 という bucket を使用する場合には,example.com と bucket1.example.com の 2 つを DNS サーバに登録しておきます.

簡易の動作確認のための unbound への設定例:

    local-data: "example.com A 192.168.1.6"
    local-data: "bucket1.example.com A 192.168.1.6"

3. Riak CS の SSL 機能を有効にして起動させる

下記のドキュメントを参考にして,Riak CS を起動させます.

また,今回はドメインを example.com で設定しているため,/etc/riak/app.config の下記の箇所も変更します.

<               {cs_root_host, "s3.amazonaws.com"},
---
>               {cs_root_host, "example.com"},

4. curl を使用して,admin ユーザのアカウントを作成する

--cacert オプションに,サーバ証明書を検証する CA 証明書を指定します.

# curl --cacert ca.pem -H 'Content-Type: application/json' -X POST https://example.com:8080/riak-cs/user --data '{"email":"admin@example.com", "name":"admin user"}'
{"email":"admin@example.com","display_name":"admin","name":"admin user","key_id":"BSNKJWA28Q9QYHRAQZVS","key_secret":"I3N_Nfh58X_Ypva4nb7_NtY-0p1RrEQabKKNtg==","id":"9062458e400b4faa5052dc8aae54dd5da58d38e6f958e555570804cc73af9588","status":"enabled"}

5. AWS SDK for Ruby をインストールする

gem で入れます.

$ gem install aws-sdk

6. ファイルをアップロードする

ファイルアップロードの例です.

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

require 'aws-sdk'

ACCESS_KEY_ID = 'BSNKJWA28Q9QYHRAQZVS'
SECRET_ACCESS_KEY = 'I3N_Nfh58X_Ypva4nb7_NtY-0p1RrEQabKKNtg=='
BUCKET_NAME = 'bucket1'
FILE_NAME = 'foo.txt'

s3 = AWS::S3.new(
  access_key_id: ACCESS_KEY_ID,
  secret_access_key: SECRET_ACCESS_KEY,
  s3_endpoint: 'example.com',
  use_ssl: true,
  s3_port: 8080,
  ssl_ca_file: '/tmp/ca.pem',
  ssl_verify_peer: true
)
bucket = s3.buckets.create(BUCKET_NAME)
object = bucket.objects[FILE_NAME]
object.write(File.open(FILE_NAME, 'rb'))

Riak CS のログ(/var/log/riak-cs/access.log)を見てみると PUT できていることが分かります.

192.168.1.104 - - [16/Aug/2013:15:29:07 +0000] "PUT /buckets/bucket1 HTTP/1.1" 200 0 "" "aws-sdk-ruby/1.15.0 ruby/2.0.0 x86_64-darwin12.3.0"
192.168.1.104 - - [16/Aug/2013:15:29:07 +0000] "PUT /buckets/bucket1/objects/foo.txt HTTP/1.1" 200 0 "" "aws-sdk-ruby/1.15.0 ruby/2.0.0 x86_64-darwin12.3.0"

7. boto で確認してみる

AWS SDK for Ruby で確認してもいいのですが,他のツールで確認してみます.

.boto の設定:

[boto]
ca_certificates_file = "/tmp/ca.pem"
https_validate_certificates = True
#!/usr/bin/env python

from boto.s3.connection import S3Connection

conn = S3Connection(
  aws_access_key_id = "BSNKJWA28Q9QYHRAQZVS",
  aws_secret_access_key = "I3N_Nfh58X_Ypva4nb7_NtY-0p1RrEQabKKNtg==",
  is_secure = True,
  host = 'example.com',
  port = 8080)

bucket_name = 'bucket1'
bucket = conn.get_bucket(bucket_name)

key = bucket.get_key("foo.txt")
key.get_contents_to_filename("bar.txt")

期待通りに,アップロードした foo.txt の内容で bar.txt が作成されていました.

念のため,Riak CS のログも確認

192.168.1.104 - - [16/Aug/2013:15:29:29 +0000] "GET /buckets/bucket1/objects?max-keys=0 HTTP/1.1" 200 252 "" "Boto/2.10.0 (darwin)"
192.168.1.104 - - [16/Aug/2013:15:29:29 +0000] "HEAD /buckets/bucket1/objects/foo.txt HTTP/1.1" 200 14 "" "Boto/2.10.0 (darwin)"
192.168.1.104 - - [16/Aug/2013:15:29:29 +0000] "GET /buckets/bucket1/objects/foo.txt HTTP/1.1" 200 14 "" "Boto/2.10.0 (darwin)"

SPDY パケット読むまでのメモ

最近は SPDY プロトコルが流行っているようなので,"SPDYと「やったー、net-http-spdyできたよー」の話 - I am Cruby!" の記事の net-http-spdy を動かして,パケットを取得しようとしてみたら,手間取った点がいくつかありましたので,設定した内容等を適当に書いておきます.

実行

example を参考にして,上記環境のサーバにアクセスしてみる

とりあえず,下記のようにしてみました.

  require 'net/http/spdy'

  fetch_threads = []
  Net::HTTP::SPDY.start(uri.host, uri.port, use_ssl: true) do |http|
    10.times do
      uri = '/index.html'
      req = Net::HTTP::Get.new(uri)
      fetch_threads << Thread.start do
        http.request(req)
      end
    end
    fetch_threads.each(&:join)
  end


実行すると下記のエラーになりました.

  /opt/ruby/2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:917:in `connect': SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed (OpenSSL::SSL::SSLError)

Wireshark でも見てみます.
f:id:Hexa:20130126185718p:plain

  Alert (Level: Fatal, Description: Unknown CA)

ルート CA 証明書を指定していないので当然ですね.

ca_file オプションを指定してみる

下記のように ca_file を追加します.

  Net::HTTP::SPDY.start(uri.host, uri.port, use_ssl: true, ca_file: './ca.pem') do |http|

エラーにならずに,Application Data の送受信が繰り返され続けます.

  $ ruby spdy.rb
  

ここで,Wireshark で Application Data を復号して,送受信されているパケットを確認しようとしてみましたが,Wireshark に秘密鍵を設定しても復号できませんでした.

そこで,Server Hello を見てみると,Cipher Suite は下記になっていました.
f:id:Hexa:20130126203653p:plain

  Cipher Suite: TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 (0x009f)

DHE の場合は復号されないのかな?

ciphers オプションを指定してみる

とりあえず,Cipher Suite を変更してみます.

Cipher Suite は,クライアントが Client Hello で Cipher Suites をサーバに送って,
その中から 1 つを選んでサーバが Server Hello でクライアントに送るので,
クライアントから送る Cipher Suites に,使用したい Cipher Suite だけを指定するか,
または,サーバ側で,使用したい Cipher Suites だけを返すように設定します(mod_ssl の場合は SSLCipherSuite ディレクティブを設定します).

SSLCipherSuite ディレクティブについては,下記を参照してください.


今回はクライアントからの Cihper Suite をしてしてみます.

下記のように ciphers を追加します.

Net::HTTP::SPDY.start(uri.host, uri.port, use_ssl: true, ca_file: './ca.pem', ciphers: 'AES256-SHA') do |http|

Wireshark で確認してみます.

Client Hello
f:id:Hexa:20130126203505p:plain

Cipher Suites には下記の 2 つが含まれていました.

  TLS_RSA_WITH_AES_256_CBC_SHA (0x0035)
  TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff)

TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff) は,SSL および TLS の renegotiation の脆弱性対応です.

renegotiation の脆弱性や TLS_EMPTY_RENEGOTIATION_INFO_SCSV の詳細については,下記を参照してください.

Server Hello
f:id:Hexa:20130126203446p:plain

Cipher Suite が TLS_RSA_WITH_AES_256_CBC_SHA (0x0035) であることが確認できます.

復号(ssl でフィルタ)
f:id:Hexa:20130126185350p:plain

"Decrypted SSL data" のタブが増えていますので,spdy パケットを確認できるようになっています.
f:id:Hexa:20130126203533p:plain

LXC の導入

Ubuntu 12.04.1 LTS に LXC を導入したので、設定内容を書いておきます

Bridge の設定

  ## Bridge 用のインターフェースを追加
  brctl addbr br0
  ## bridge forward delay に 0 秒を設定する
  brctl setfd br0 0
  ## 物理インターフェースの eth0 に Bridge のインターフェース br0 をバインドする
  brctl addif br0 eth0
  ## eth0 をアクティブにする
  ifconfig eth0 0.0.0.0 up
  ## DHCP で br0 に IP アドレスを割り当てる
  dhclient br0

LXC

インストール
  # aptitude install lxc
設定

/etc/lxc/lxc.conf

  lxc.network.type = veth
  lxc.network.flags = up
  lxc.network.link = br0
  lxc.network.ipv4 = 0.0.0.0/24
  lxc.network.name = eth0
コンテナの作成

  # lxc-create -f /etc/lxc/lxc.conf -t ubuntu -n u200
  • -f: 設定ファイル
  • -t: テンプレート
  • -n: コンテナ名
コンテナの起動

  # lxc-start -n u200
  • -n: コンテナ名
コンテナのシャットダウン

  # lxc-shutdown -n u200
  • -n: コンテナ名
コンテナのクローン

  # lxc-clone -o u200 -n 2001
  • -o: クローン元のコンテナ名
  • -n: クローン先のコンテナ名
自動起動

ホスト起動時に自動的にゲストを起動させるには、/etc/lxc/auto/ 以下にゲストの config のシンボリックリンクを張ります

  # ln -s /var/lib/lxc/u200/config /etc/lxc/auto

Unbound

ちょっとした検証をするときに,DNS サーバが欲しくなることがよくあるのですが,BIND をインストールして,設定して,,が面倒になってきたので,簡単に準備できそうな,Unbound をインストールして試してみました

インストール

# aptitude install unbound

設定

/etc/unbound/unbound.conf を下記のように設定します

server: 
    interface: 0.0.0.0
    port: 53
    access-control: 192.168.1.0/24 allow

    local-data: "app1.hexa.diary A 192.168.1.2"
    local-data: "app2.hexa.diary A 192.168.1.3"
    local-data: "app3.hexa.diary A 192.168.1.4"

local-data: を設定すれば,設定した内容で A レコードを返してくれます
設定は,この 1 ファイルで済むので簡単です

dig で確認

$ dig @192.168.1.10 app1.hexa.diary

; <<>> DiG 9.8.3-P1 <<>> @192.168.1.10 app1.hexa.diary
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 8789
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;app1.hexa.diary.		IN	A

;; ANSWER SECTION:
app1.hexa.diary.	3600	IN	A	192.168.1.2

;; Query time: 41 msec
;; SERVER: 192.168.1.10#53(192.168.1.10)
;; WHEN: Tue Jan 15 00:55:29 2013
;; MSG SIZE  rcvd: 49