どうも。最近は某社からNEMが盗まれた事件がホットですね。
ここでは、取引所にかぎらず仮想通貨を扱うサービスを運営しているときに、これだけは設定しておきたい、というセキュリティ対策を解説していきます。
あくまで私自身の価値観です。
「以下の通り対策をしていたのにハッキングで盗まれた!責任取れ!」と言われても私は取れません。
coind編
coindとは、bitcoind
などを含む、仮想通貨で使用されるノードのソフトウエアの総称です。仮想通貨を扱う場合は、基本的にこのcoindが必要になります。
Bitcoinの場合は/usr/bin/bitcoind
、Monacoinは/usr/bin/Monacoind
があります。Ethereumの場合はgethという名前になっています。
ちなみに、このdはlinuxなどでデーモンによく使ってるのを模範した…んだと思います。httpdとかmysqldみたいな。
また、coindはC++製ですが、その他有志によって作られたビットコインノード用のソフトウエアがあります。有名なものでは、Bitcore(node製)、Bitcoinj(Java製)、btcd(go製)があり、また他の通貨でもそれぞれの改造版が開発されてるものもあります。
coindは、JSON-RPCという、JSONを直接TCP/HTTPに乗っけたような通信プロトコルを使用しています。このJSON-RPCをPHPやPython、Railsから直接/ライブラリで間接的に叩くことで、送金、アドレス生成、署名などを行います。
以降の章は、bitcoindを前提として書いていますが、bitcoinに準拠した他の通貨のcoindに置き換えても通じます。
ネットワーク(ポート)
Bitcoindは、ビットコインのノードの役割をするサーバーです。通常はサービスとしてサーバーの中で動作し、APIを提供します。
また、他のノードと相互に接続する役割もあります。
APIの場合は、TCP 8332
、ノード間接続はTCP 8333
のポートを使用します。
このため、スイッチやファイアウォールなどではTCP8333のみを開放・ルーティングし、8332番は原則非公開としましょう。
ネットワーク(サブネット)
Bitcoindは、設定(bitcoin.conf)のrpcallowip
という項目より、APIサーバーが通信を受け入れる(listenする)ネットワークを指定できます。
127.0.0.1
の場合は自分自身からの接続のみ、x.x.x.x/yy
は、対象のネットワークからのみ接続を許します。適宜変更してください。
JSON-RPC 認証(パスワード)
JSON-RPCは、ユーザー名とパスワードを使う単純な認証を行います。
このユーザー名とパスワードは、設定(bitcoin.conf)に直接書き込むことで行なえます。なので、bitcoin.confの権限は可能な限り600などにしておくべきです。
また、このパスワードが漏れれば、ノードを完全に操作できるため、APIを叩かれてすべての仮想通貨を送金させられてしまいます。
パスワードは長く、複数の文字種を含むようにしてください。
JSON-RPC用のユーザー名とパスワードは、昔はbitcoindのインストール時に生成されましたが、現在は何も生成されません。自力で作成するか、次の章のcookieを利用してください。
JSON-RPC 認証(cookie)
最新のbitcoindでは、デフォルトでJSON-RPCにパスワード認証を使用しません。cookieファイルというものを使用します。
cookieファイルは、bitcoindが起動するときに毎回再生成され、中にはランダムな長い文字列が記録されています。
APIを叩く側はパスワードのかわりにcookieファイルの内容を使用することができます。
これにより、cookieファイルを読むことができるプロセス=bitcoindにアクセスできる、といった構図を作れます。
HDウォレット
HDウォレットは、一つの公開鍵・秘密鍵ペアから、それぞれ子となる鍵を導出できる技術です。これにより、マスター秘密鍵のみ厳重に保管しておいて、普段は子or孫の鍵を使用することができます。これにより、バックアップする鍵は一つだけでよく、さらにその鍵は普段は使用しなくてよくなるので、セキュアな鍵の利用ができます。
コールドウォレット
コールドウォレットは、インターネットに接続しないノードです。ノードとしての機能はあまり使用せず、署名機械として使用します。
送金に必要な電子署名をオフラインコンピューターで行うことで、人間の手が物理的に動かない限り送金できないようになります。
手順は以下のようになっています。
- なんらかの影響で、ホットウォレットの残高が少なくなった
- ホットウォレット側で、コールドウォレットからホットウォレットに対する送金トランザクションを生成する(createrawtransaction)。もちろん作っても署名がない≒正しくないため、無効。
- 2.で作ったトランザクションのバイナリを、16進数ダンプでもして、紙に書き写す。もしくは似たようなアナログな方法でメモ。
- コールドウォレットにモニターとキーボードを接続し、メモを見ながらコールドウォレット内で動いているbitcoindの署名に関するAPIを叩き(signrawtransaction)、3.のトランザクションに署名を追加してもらう。
- 4.でできた正しいトランザクションを、また紙にメモする。
- 5.を、ホットウォレットなどの通常のウォレットに頼んでネットワーク全体にブロードキャストしてもらう(sendrawtransaction)
これで、安全に資金を移動できます。安全な分、非常にめんどくさいです。
なので、本当に普段使わない分はなるべくコールドウォレットにおいておきましょう。
また、trezorなどのハードウェアウォレットをコールドウォレットに使うのも一つの手です。
マルチシグネチャ
マルチシグネチャは、名前の通り複数人の署名が無いと送金できないトランザクションを生成できます。
手順は以下のとおりです
- マルチシグ用のアドレスを作る(addmultisigaddress)
- 送金したいときは、マルチシグに参加したユーザーの署名を集める
m人中n人、といった人数が自由に選べるので、たとえば5人中5人や、3人中1人の署名が必要、なんていうアドレスも生成できます。
また、コールドウォレットと組み合わせて使うこともできます。
データディレクトリ
bitcoindは、ブロックチェーンにデータと、秘密鍵やアドレスの情報をデータディレクトリと呼ばれる場所に保管します、~/.bitcoin
や/etc/bitcoin
など、インストール方法によってその場所は様々です。
ブロックチェーンのデータは、いくらでもネットワークからダウンロードできますが、秘密鍵やアドレス・アカウントの情報はwallet.datをなくすことで一切取り返しがつかなくなってしまいます。
バックアップを取っておきましょう(サーバー編を参照)。
サーバー編
coindやwebサービスなどを動かすサーバー部分についてです。
隔離/VM/コンテナ
送金を担う役割であるcoindと、外部からのアクセスを受けるwebサーバーは、ホスト自体を別のモノに分けるべきです。
しかし、コンテナ技術や仮想マシンを使うことで、一つのサーバーの中で、それぞれのサービスとcoindを隔離することができます。
Docker、LXC、jail、KVMなどを使ってみましょう。
また、コンテナ機構を使いたくない、かつCPUに仮想化技術が無い場合、jailやchrootも一つの手です。
ディスク
bitcoindが利用するデータディレクトリは、権限をサービス用のユーザー以外からread/writeができなようにするべきです。600などを推奨します。また、できればディスク自体を暗号化することをおすすめします。
バックアップ
サーバーの秘密鍵である「wallet.dat」は、一番重要なものですが、どうしてもバックアップが必要となります。
ネットワークを経由するバックアップは、現状は限りなく安全ですが、やはり心配です。
その場合はUSBメモリにでも入れて限られた人間の指紋でしか開けられない耐火金庫にでもしまっておきましょう。
また、秘密鍵を覚えずに、任意の英単語の組み合わせで鍵を再生成できる、BIP44パスフレーズといった仕組みがあります。これを書き留めておくのも手です。モバイルウォレットなどでよく目にしますね。
SELinux/強制アクセス制御
よくサーバー構築入門で真っ先に無効にされてしまうSELinuxですが、もし知識や経験があるなら有効にするのも一つの手です。
そして、bitcoindのデーモン本体やデータぃレクトリに対するポリシーは有志がネットに公開しています。短いので、全文自分の目で読んだ上で導入しましょう。
物理
最低限、コールドウォレットなどを動かすサーバーでは、BIOSのパスワードとUSBの無効化、外部端子のボンド埋めくらいしといたほうが良い気がします。
サーバーの場所
AWSやAzureなどを使いたくなりますが、コールドウォレットだけは、限られた人間のみ触れる場所にコンピューターを設置して、ホットウォレットはクラウドを利用していいと考えています(あくまで個人の意見です)。
コールドウォレットはラズパイなどでも良いかもしれません。
ネットワーク
当たり前ですが、coindやサービスが動くネットワークと、開発に使用するマシンなどのネットワークは完全に隔離しましょう。社内や身内からの不正アクセスを考えれば、DMZなども利用せず、専用のパブリックアドレスを与えた空間を作るべきです。
また、どうしても有人アクセスをする場合、SSHをする踏み台専用のサーバーを作り、そこからトンネルを掘ったりsshエージェント転送をするようにしましょう。
sshをする場合は、ssh鍵にパスフレーズを書けておくのはもちろん、opensshにGoogle Authoricaterなどの2段階認証を有効にできるので、使っておくと良いでしょう。
普段のデプロイは人の間違いが起きないように、自動で行えるとなお良いです(jenkins、travis、gitなど)
Webサービス
Webサーバー
NginxやApache、その他RailsやDjangoなど、様々な方式がありますが、大体の環境でcoindを叩くためのライブラリが有志によって作られています。これは実際に便利ですが、なんでもかんでも使うことはあまりおすすめしません。
OSSをサービスに使うべきか否か、といった問題がありますが、JSON-RPCは本当に単純なものです。
なので、基本的なセキュリティ対策(XSS→パースをする)をした上で、ライブラリのソースコードを読んで良いところを取り入れながら、簡単なラッパースクリプトを自作するぐらいにとどめておくのが良いと思います。
ただ、コア開発者が公式で公開しているものや、ある程度の知名度があるものは利用しても構わないと思います。特にC++のライブラリはbitcoindのソースコード自体に含まれて居ます。
※あくまでも個人的な意見です。
テスト
coindには、testnetとよばれる、いわゆるテスト開発をする専用のブロックチェーンが存在します。これは世界中の人で共有されており、通常のメインチェーンとは技術的には全く変わりがないものです。
bitcoin.confにtestnet=1と追記するだけで立ちがります。
また、testbet以外にもreg testnetと呼ばれる、自分で設定した任意のマシンしか接続できないブロックチェーンも作成できます。これは自分の構築したcoind間(一台だけならそれだけ)でのみ有効になるため、外部に情報が送信されることはありません。また、中央集権なため、自分の好きなタイミングでCPUを消費せずにブロックを生成(マイニング)できます。これにより、テストのタイミングも自由に変更することができます。
まずはregtest番でテスト→testnetでテスト→mainnetでテスト→デプロイ、といった方式がデファクトスタンダードのように感じます。
bitcoin-cli, bitcoin-tx
bitcoindには、/usr/bin/bitcoin-cli
と/usr/bin/bitcoin-tx
といった名前のコマンドが付属してきます。cliはJSON-RPCをコマンドラインから操作するためのもの、bitcoin-txはトランザクションやブロックの情報をコマンドラインから確認するためのものです。
サービスをいきなり開発する前に、コマンドラインでJSON-RPCを触ってみるとイメージがつかみやすいかもしれません。
ユーザーの残高管理
単純にユーザーの残高を管理したい場合があります。最初はビットコインアドレスと内部のユーザーidをRDBMSなどで紐付けたくなってしまいますが、実はcoindにその機能があります。
coindには、任意の文字列とアドレスを紐付ける「アカウント」という仕組みがあります。ユーザーIDなどでアカウントを作っておけば、そのアカウントを指定して送金ができます。
こうしておけば、最低限wallet.datさえ生き残っていればデータの救出ができます。
最後に
最低限、と書きましたが、これを最低限とみなすか、やりすぎとみなすかは人によって感覚が違うと思います。それこそ簡単な投げ銭サイトを作る場合はこれほどの対策は必要ないと思いますし、取引所などでは本当に最低限にしかなりません。
本当にセキュアな環境が求められるようなサービスを作る場合は、ブロックチェーンに精通している人間と銀行などでセキュリティ部門に所属するような人をそれぞれ捕まえてくるのが一番かと思います。
おのかちおさん、こんにちは。(お久しぶり?)
コールドウォレットやマルチシグニチャにしても、システム内に侵入されるなど送金先のアドレスを改ざんされると、多分、ダメ。
trezorなどのハードウェアウォレットが安全なのは、送金先のアドレスを確認できるから。
取引所で、そのあたりを、どう安全にするのかが、ポイントかもと思って眺めています。
利用者が送金先を確認できるハードウェアウォレットを手順に組み込めば、安全かなと思っているけど、まずはネットワークセキュリティ屋が頑張って、それなりに安全なシステムを考えるのかなと。
@izuna
たしかに、アプリケーション側が攻撃されたら元も子もないので、そこがポイントですね。