CookieのNoHttp属性の提案仕様

以前、「Cookieの仕様改定版、RFC6265bisの議論」でも書いたとおり、IETFのHTTPBis WGではCookieのセキュリティ向上に向けて改訂作業が行われています。

Cookieの改訂版のAuthorでもあるGoogleのMike West氏が、それとは別に新しくCookieに「NoHttp」属性を追加する新しい「Non-HTTP Cookies」という提案仕様を書いています。

この属性の付いたCookieは、サーバには送信されなくなります。

Non-HTTP Cookies

背景

良くも悪くもCookieはSame-Originを超えて共有できます。analyticsのサービスなどは、document.cookieを通して他のページとユーザの状態を共有するものがあります。しかしこの場合は、document.cookieでアクセス出来れば良いようで、HTTPリクエスト送信時にCookieヘッダに付与する必要ないケースが有るようです。(Mike West氏のツイート)


つまり、無駄にCookieを送信していることになり、通信量的にもセキュリティ的にも余計な事をしていることになります。

そこで、それを抑制するNoHttp属性が出てきます

NoHttp属性

NonHttp属性の付いたクッキーはサーバには送信されないようになります。

例えば以下のように設定します

document.cookie = "name=value; Secure; NonHttp";

HTTPヘッダでも同様に設定できます

Set-Cookie: name=value; Secure; NonHttp

但し、もちろん対応していないクライアントはこのフラグを無視するためHTTPリクエストの際に付与されても良いようにしておく必要はあります。

その他

名称については、NoHttp、DOMOnly、DocumentCookieOnlyなどの変更の可能性があるようです。

また、著者が「Cookieは "HTTP State Management" 機能ですが、そこからHTTPを除くユースケースというのは奇妙だが、インターネットは奇妙な場所である」と書かれているのも、また印象的でした。

署名式 SRI(Subresource Integrity)の議論

W3CのWeb Application Securityワーキンググループで、署名式 SRIの議論が出ていたので簡単に書く

背景

セキュリティ向上の目的で、Webページ上で読み込まれるリソース(JavaScript等)を制限する方法に、CSP(Content Security Policy)やSRI(Subresource Integrity)と言った技術がある。

CSP

CSPを利用することで、読み込むJavaScriptを信頼できるURLのみに制限出来る。

CSPのscript-srcディレクティブで、以下のように制限できる。

script-src https://example.com/script/trusted.js


しかし、CSP Is Dead, Long Live CSP! On the Insecurity of Whitelists and the Future of Content Security Policyで述べられているように、URLベースの制限は非常に細かく記述する必要があり、大規模なサービスでちゃんと運用することは難しい面があるようです。

SRI

SRIを利用することで、リソースが本当に意図したものである場合のみ読むこむように出来ます。

scriptタグのintegirty属性にコンテンツのハッシュ値を付与することで、リソースの正当性が評価されます。これは、CDNなどを利用してる時に、CDNによる悪意ある改ざんから防ぐことが出来ます。

<script src="https://example.com/example-framework.js"
        integrity="sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7"
        crossorigin="anonymous"></script>

しかし、SRIはリソースの更新の順番に課題があり、リソース自体の更新とscriptタグ側のintegrity値の更新を行われなければなりません。

署名式 SRI

W3CのWeb Application Securityワーキンググループのメーリングリストで、GoogleのMike Westより「Proposal: Signatures in SRI.」として投げかけられました。

まだ、議論の段階だが概要についても、W3Cのリポジトリで公開されている。

Ed25519公開鍵で署名し、リソースの正当性を担保する。scriptタグ側に公開鍵を、HTTPレスポンスのIntegrityヘッダに署名値が含まれる形である。

javascriptタグ

<script src="https://my.cdn.com/whatever.js" integrity="ed25519-[base64-encoded public key]">

HTTPレスポンス

HTTP/1.1 200 OK
Accept-Ranges: none
Vary: Accept-Encoding
Content-Type: text/javascript; charset=UTF-8
Access-Control-Allow-Origin: *
...
Integrity: ed25519-[base64-encoded result of Ed25519(`console.log("Hello, world!");`)]
 
console.log("Hello, world!");

この方式であればCDNを利用していても、与えられた公開鍵で署名値を確認することで、意図してないリソースを読み込む(実行する)事は防がれる。また、javascriptタグ側の変更は不要である点もメリットとしてあり、これでSRIにあった更新順序の問題は解決される。


実際に需要があるのか?古いバージョンのファイルを提供する攻撃が可能か?鍵管理が困難ではないのか?PKIやX.509を利用したほうが良いのではないか?といった疑問については、上記でも上げた概要に書かれている。

Apache2のmod_proxy_http2を試す

去年ぐらいからApache2.4にも実装されたmod_proxy_http2を今更ながら試す。

mod_proxy_http2はリバースプロキシとして動作する際に、バックエンドのサーバと通信する際にもhttp2を使えるようにするモジュールである。

f:id:ASnoKaze:20170531004524p:plain


今回はxenialを使うが、通常降ってくるapache2だとこの機能は入っていないのでざっとtrunkをビルドする。

ビルド

#ちょっと余分なのもあるが
$ apt-get install libnghttp2-dev libexpat1-dev gcc g++ libpcre3-dev libcunit1-dev libev-dev \
                libjansson-dev libjemalloc-dev cython make binutils autoconf automake autotools-dev \
                libtool pkg-config zlib1g-dev libssl-dev libxml2-dev libevent-dev python3.4-dev libevent-openssl-2.0-5


$ svn checkout http://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x httpd-2.4.x
$ cd ../httpd-2.4.x/
$ svn co http://svn.apache.org/repos/asf/apr/apr/trunk srclib/apr
$ ./buildconf

$ ./configure  --enable-proxy-http2 --enable-http2 --enable-proxy-http
$ make

$ sudo make install

設定

# /usr/local/apache2/conf/httpd.conf
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule http2_module modules/mod_http2.so
LoadModule proxy_http2_module modules/mod_proxy_http2.so

Protocols h2 h2c http/1.1
H2Direct on

ProxyPass "/app" "h2c://localhost"

確認

#起動
$ sudo /usr/local/apache2/bin/httpd

#アクセス
$ curl localhost/app

#ログ
$ tail /usr/local/apache2/logs/access_log
127.0.0.1 - - [30/May/2017:15:51:50 +0000] "GET / HTTP/2.0" 200 45
127.0.0.1 - - [30/May/2017:15:51:50 +0000] "GET /app HTTP/1.1" 200 45

http/1.1でアクセスした際に、自身へのプロキシはh2で行ってることが確認できた。

簡単。

あとは、コネクション使いまわすか後日確認する
=> 子プロセス毎にコネクションを再利用するように見えました。多分、再利用条件はRFCの通り(h2cであれば、IPが一緒のことだが、もともとの:authorityじゃないからちょっとよくわからない)

DNS ANAMEレコードの提案仕様

20181019追記
Address-specific DNS aliases (ANAME)」として、新しい draft-02が出されました


20180912追記
この提案仕様はexpireしてしまいました。
完璧に代替するものではありませんが、いくつかの方法について議論がされているようです。


IETFでのDNS関連の標準化は普段追ってないので恐縮だが、DNSOP(DNS OPerations)WGで気になったドラフトがあったので読んで見る。

Address-specific DNS Name Redirection (ANAME)」という提案仕様であり、先日WGドラフトになったようだ。著者は、ISC, PowerDNS, DNSimple の方々でありDNS実装に関わっている人が書いているようだ。

PowerDNSでは、すでにALIASと言う似たような機能が実装中のようですし、CloudflareやAkamaiとも議論している様子が伺える。

背景とCNAME

DNSにはホストの別名を定義するCNAMEレコードという機能があります。このCNAMEレコードは、同一のホスト名(ドメイン)に対して他のレコードと共存することができません。

このようにAレコードとCNAMEレコードを併記することはできません。

hoge        IN      CNAME   fuga
hoge        IN      A       192.168.0.1

そこで特に問題になるのが、Zone Apex(ドメイン名そのもの)のレコードでしょう。example.comというドメインで考えてみましょう。ドメイン名そのものは設定上 @ で記述します。通常は、SOAレコードや、NSレコード、その他のTXTレコードを記述するケースが多いでしょう。

@   IN SOA ns1 postmaster (
                  ...
    )

    IN NS ns1
    IN NS ns2

もちろん、これらのSOAレコードやNSレコードと、CNAMEは併記できません。そのため、@に対してCNAMEは使えないでしょう。CDNを利用する場合はCNAMEでCDNのレコードを参照することが多いですが、example.com自体にCNAMEは指定できないので、CDNは利用できない事になります。

@   IN CNAME fuga

また、ラウンドロビン的に複数のCDNを出し分ける事もできないでしょう。

ANAMEレコード

ANAMEレコードは、CNAMEレコードのように動作しますが、AやAAAAレコードへの問い合わせのみリダイレクトを実施します。そのため、他のレコードと併記することができます。

これを利用することで、Zone ApexでもCDNの利用が可能になります。

権威サーバは、クライアントがANAMEレコードに対応しているかわからないので、自信でANAMEレコードを解決してAレコードと一緒に応答してもよいようです。

仕様

仕様には、キャッシュやDNSSECなど、さらに詳しく権威サーバ及びリカーシブサーバの振る舞いについて書かれています。


参考 (勉強になりました)

NginxのstreamモジュールがDTLSに対応した(パッチ)

NginxのL4レイヤのProxy機能である、streamモジュールのDTLS対応パッチが公開されていたので試す。
(まだ検証段階で、フィードバック募集中らしい)

簡単に言うと、以下のようににDTLS -> Plain UDPや、その逆にPlain UDP -> DTLSへのProxyが出来る。

DTLS -> Plain UDP

f:id:ASnoKaze:20170524014622p:plain

Plain UDP -> DTLS

f:id:ASnoKaze:20170524022625p:plain

Plain UDP -> DTLS -> Plain UDP

2つ組み合わせれば、中間だけDTLSで暗号化することも出来る
f:id:ASnoKaze:20170524014630p:plain

ビルド

今回は、Ubuntu Trustyでビルドする

sudo apt-get install libpcre3-dev

wget http://nginx.org/download/nginx-1.13.0.tar.gz
wget http://nginx.org/patches/dtls/nginx-1.13.0-dtls-experimental.diff

cd nginx-1.13.0
patch -p1 -i ../nginx-1.13.0-dtls-experimental.diff

./configure --with-stream --with-stream_ssl_module --with-debug
make

sudo make install

いつも通り、鍵も準備

openssl genrsa 2048 > server.key
openssl req -new -key server.key > server.csr
openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt

DTLS -> Plain UDP

設定
#/usr/local/nginx/conf/nginx.conf
stream {
    # please enable debug log
    error_log logs/error.log debug;

    server {
        # add 'udp' and 'ssl' simultaneously to the listen directive
        listen 127.0.0.1:4443 udp ssl;

        # enable DTLSv1 or DTLSv1.2 or both protocols
        ssl_protocols DTLSv1;

        # set up other SSL options as usually
        ssl_certificate server.crt;
        ssl_certificate_key server.key;

        proxy_pass 127.0.0.1:8080;
    }
}
動作確認

クライアント

$ openssl s_client -dtls1 -connect 127.0.0.1:4443
test

バックエンド

$ nc -ul 8080
test

Plain UDP -> DTLS

設定
stream {
    server {
        listen 127.0.0.1:5555 udp;

        # enable SSL to proxy
        proxy_ssl on;
        # enable DTLSv1 or DTLSv1.2 or both protocols
        proxy_ssl_protocols DTLSv1;

        # set up other proxy SSL options as usually
        proxy_ssl_certificate  server.crt;
        proxy_ssl_certificate_key server.key;

        # the backend is a DTLS server
        proxy_pass 127.0.0.1:4433;
    }

}
動作確認

クライアント

$ echo -en "hello"| nc  -u localhost 5555

バックエンド

$ openssl s_server -cert server.crt -key server.key -dtls1 -accept 4433
ACCEPT
...(略)
hello
その他

省くが、Plain UDP -> DTLS -> Plain UDPの変換も上記の設定の組み合わせで実現できた。
使える機会があるかわからないが、普段使ってるNginxでDTLSが受けられるのは結構楽そうだ

RFC8174「RFC 2119のキーワードにおける大文字と小文字の曖昧性」

RFCやInternet-Draftを読んだことある人であれば、文中にMUSTやSHOULDといった大文字のキーワードが用いられてるのを見たことあるかと思います。

たとえば、このように

An endpoint MUST treat this as a stream error (Section 5.4.2) of type PROTOCOL_ERROR.


これらのキーワードはプロトコルの仕様の曖昧性を無くすために 「RFC 2119 Key words for use in RFCs to Indicate Requirement Levels」で定義されています。

しかし、微妙な曖昧性があるためRFC 8174で更新されました。

RFC8174 Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words

「MUST」「SHOULD」「MAY」といった要請の程度を示すキーワードを定義するRFC 2119ですが、その文章にはこう書かれています。

「These words are often capitalized」
(これらの語句は、よく大文字になっています) IPAの翻訳より

このoftenによる微妙な表現のため、"must"や"should"といった小文字の場合にどう解釈するか微妙な曖昧性があります。

RFC8174では下記2点が明記されるようになりました

  • これらの語句は全て大文字の場合のみ、この文書で定義された特別な意味をもつ
  • 小文字の場合は通常の英語の意味をもち、この文書の影響はありません

また、絶対にこれらの語句を使用すべきというわけではない旨が追記されたほか
別の文書から参照する時の例が更新されています

      The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
      NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED",
      "MAY", and "OPTIONAL" in this document are to be interpreted as
      described in BCP 14 [RFC2119] [RFC8174] when, and only when, they
      appear in all capitals, as shown here.

TCP Fast Openの闇と、Kernelの緩和コミット

TCP Fast Open

TCP Fast Openと呼ばれる技術があり、RFC 7413として標準化されている。

このTCP Fast Openを使うと、一度コネクションを貼った相手とは、TCPの3ウェイハンドシェイク中にデータを送受信できるようになる。クライアントからSYNとともにデータを送信することで、実際にデータを送受信開始するまでの待ち時間が短縮できる。

Linuxではすでにクライアント/サーバ両方でTCP Fast Openを使用できる。

TCP Fast Openの闇

しかし、数年前よりこのTCP Fast Openには一部のネットワークで奇妙な振る舞いをすることが知られている。Appleの人が実際にデプロイした時に見つけたもので、IETFやNANOGにて報告されており、その時の資料は下記のとおりである

(0.1%の環境で発生したと発表時の質疑にもある)

一部を抜粋すると

TCP Fast OpenのSYN, SYN/ACKは通るが最後のACKがブロックされる
f:id:ASnoKaze:20170509215524p:plain

3ウェイハンドシェイクは完了するが、片方向のみデータ送信に失敗する
f:id:ASnoKaze:20170509215604p:plain

この状態になると、OS的にはハンドシェイクが完了しておりタイムアウトを待ち、その後アプリケーションなどにより再送信されるという繰り返しになりうる(フォールバックされない)。

この問題はクライアントの居るネットワークのファイアウォールやIDSが原因であり、単純にはサーバやクライアントでTCP Fast Openを無効にするという対応をすることになる。

Kernelでの緩和対応

上記問題を緩和するために、Kernelに「net/tcp_fastopen: Disable active side TFO in certain scenarios」というコミットが入った。

基本的には、クライアントサイドにてTCP Fast Openを使ってる時に怪しい挙動があった場合は、globalにTCP Fast Openを無効にする。最初はデフォルトで1時間無効化し、次に失敗したときは2時間と倍々で時間が長くなっていく仕組みになっている。通信に成功した場合は、待ち時間は最初の1時間にリセットされる。待ち時間は、tcp_fastopen_blackhole_timeout_secで設定することも出来る。

globalに無効になるのは、クライアント側のネットワークが原因であることが主なので、全体として向こうにしたほうが良いということだろう。

また怪しい挙動とは

  • クライアント側でTCP Fast Openを利用しており、データが無く、順序が正しくないRSTを受信した時
  • クライアント側でTCP Fast Openを利用しており、順序が正しくないFINを受信した時


コメントを読む限りサーバ側では現状特別なハンドルはしないようだ。大きいサービスだと、TCP Fast Openを有効にするには上記コミットが普及してからの方が良いのだろうか...(TCP Fast Openが悪いってわけじゃないんですけど)