おんぶろぐ ver.2

おんぶろぐがインチキだらけなので改心しました

読書: Beyond Corp - 1. Introduction

概要

Beyond Corp とは ゼロトラストセキュリティモデルのGoogle版実装です。

今回はその研究論文から 実現手法:「Google での設計からデプロイまで」 をご紹介したいと思います。こちらは一通りアプローチを説明した上での実装方法という内容になります。なかなか本格的ですが興味深い内容です。

そういう訳で、簡単に拙訳したものをベースに要点だけメモしたものとなります。

目的

The goal of Google’s BeyondCorp initiative is to improve our security with regard to how employees and devices access internal applications. Unlike the conventional perimeter security model, BeyondCorp doesn’t gate access to services and tools based on a user’s physical location or the originating network; instead, access policies are based on information about a device, its state, and its associated user. BeyondCorp considers both internal networks and external networks to be completely untrusted, and gates access to app

BeyondCorp 構想の目的、それは従業員やデバイスが内部アプリケーションにどうアクセスするかという点からセキュリティを改善することです。一般的な従来モデルと異なり BeyondCorpは物理ロケーションを基準にサービスやツールへのアクセスを制限しません。その代わりにアクセスポリシーは 別の情報源に基づきます。その情報源とはアクセスに用いたデバイス・そのデバイスの状態・そしてデバイスに紐付いたユーザです。同時に BeyondCorp では内部ネットワークと外部ネットワークの両方を全く信頼できないものと考えます。以上を前提とした上で アプリケーションへのアクセスを制限するのです。

As illustrated by Figure 1, the fundamental components of the BeyondCorp system include the Trust Inferer, Device Inventory Service, Access Control Engine, Access Policy, Gateways, and Resources. The following list defines each term as it is used by BeyondCorp:

図-1に示すとおり、BeyondCorpシステムのコンポーネントTrust Infer, Device Inventory Service, Access Control Engine, Access Policy, Gateway そして Resource で構成されます。以下のリストでは各用語がBeyondCorpでどのように利用されているかを示します。

  • Access requirements are organized into Trust Tiers representing levels of increasing sensitivity.
  • Resources are an enumeration of all the applications, services, and infrastructure that are subject to access control. Resources might include anything from online knowledge bases, to financial databases, to link-layer connectivity, to lab networks. Each resource is associated with a minimum trust tier required for access.
  • The Trust Inferer is a system that continuously analyzes and annotates device state. The system sets the maximum trust tier accessible by the device and assigns the VLAN to be used by the device on the corporate network. These data are recorded in the Device Inventory Service. Reevaluations are triggered either by state changes or by a failure to receive updates from a device.
  • The Access Policy is a programmatic representation of the Resources, Trust Tiers, and other predicates that must be satisfied for successful authorization.
  • The Access Control Engine is a centralized policy enforcement service referenced by each gateway that provides a binary authorization decision based on the access policy, output of the Trust Inferer, the resources requested, and real-time credentials.
  • At the heart of this system, the Device Inventory Service continuously collects, processes, and publishes changes about the state of known devices.
  • Resources are accessed via Gateways, such as SSH servers, Web proxies, or 802.1x-enabled networks. Gateways perform authorization actions, such as enforcing a minimum trust tier or assigning a VLAN.

  • アクセス要求は Trust Tiers(信用レベル) で管理される。ここでは増加する脅威のレベルを表現している
  • Resource はアクセスコントロールを必要とする全てのアプリケーション・サービス・インフラを一覧化したものだ。オンラインナレッジベース・財務データベース・データリンク層の接続・事務所のネットワークはその候補となる。各Resource にはアクセス時に最低限必要な Trust Tiers (信用レベル) が定義されている
  • Trust Inferer(信用示唆) は継続的にデバイスを監視しその状況を判定する。このシステムはデバイスに付与する信用レベルの最大値を決定しており、その結果に基づいて社内接続可能なVLANを割り当てる。これらのデータは Device Inventory Service に記録され、デバイスの状態に変更が入ったりデバイスからの更新通知に問題が発生すると、信用レベルの再評価が行われる
  • Access Policy は Resource、信用レベル、及びその他の情報 のプログラム表現であり、認可を成功裏に行う上で準拠しなければならないものだ
  • Access Control Engine は集中管理された ポリシーの実行サービスだ。これはゲートウェイから参照され、ゲートウェイは認可判定の際に Access Policy、Trust Inferer (信用示唆)、要求されたリソース、そしてリアルタイムな証明書を利用する
  • このシステムの心臓部では Device Inventory Service が継続的に既知のデバイスの状態変化を監視・処理・通知している
  • Resource は Gateway 経由でアクセスされる。これには SSHサーバ、Webプロキシ、802.1xの有効なネットワークが含まれる。そしてこれらは認可処理を実施する。一例としては最低限の信用レベルの強制化や VLANの割り当てなどだ

出典

実現手法:「Google での設計からデプロイまで」

読書: ゼロ・トラスト・ネットワーク - 1章

概要

本書は O'reilly から 2019年10月に出版された書籍で、テーマはネットワークセキュリティとなっています。今回読み終えた 1章は基本的な内容で、主にゼロ・トラストという考え方に関する内容となっています。

ゼロトラストは、ネットワークに信頼を置くことに伴う問題の解決を目指すものである。

要するに、従来のネットワーク間に境界を設け、危険なネットワークから分離した安全なネットワークを用意することで、脅威に対抗しようという考え方 (境界モデル) からの脱却を試みたものとなります。

境界モデルの課題

具体的な欠点

境界モデルは 城壁に例えられ、攻撃者は城壁を突破しなければ機密データにアクセスできないものの、いくつかの欠点があると指摘されています。

  • 境界内の通信は検査されない
  • ホスト配置に制約が生じる (境界内にいなければならない)
  • 境界が単一障害点となる

特に問題視されているのが最初の項目で、現代の攻撃手法に対してこの方式では一定の効果はあるものの、不十分とされています。

本質的な課題

実際、攻撃者は 悪意のあるプログラムを取り込むための小さなコード(dialer) さえ送り込めれば、マルウェアを取り込み、新たな接続を構築し、乗っ取ったコンピュータ (patient zero) を起点に境界内を蹂躙できます。これを本書では以下の様に表現しています。

突き詰めれば、境界モデルの欠点は、全体的な保護に欠けていることにある。頑丈な殻に覆われた脆弱な体のようなものである。私たちが本当に求めているのは骨と肉が詰まった堅牢な体である。

そもそも、境界モデルとは Global-IP枯渇に伴って発生した Privateネットワークが最初に限っては安全だった (外部通信の要件が少なかった) ことを発端として始まった考え方であり、現代の実情には合っていないというのが本章の主張です。

解決への示唆

先に述べられた『骨と肉が詰まった堅牢な体』のイメージとして、ゼロトラストの原則を適用した解決策となりうるアーキテクチャを提示しています。具体的なイメージを確認したい方は是非本書を手にとって確認してみてください。

感想

確かに、現代では 外部通信の要件が多く攻撃手法も進化し更にはクラウドにより境界も抽象されており、今まで成り行きで進化してきたネットワークでは防衛上の限界があるというのはとても説得力があります。

また、解決の示唆として仄めかされているアーキテクチャは、近代的なソフトウェアのデザインにどことなく類似している思いました。

例えば k8sIstio/Envoyが目指す中央でコントロールし末端で制御する設計や、GoogleのSDNであるAndromedaが目指したソフトウェアでネットワークを定義しハードウェアの性能を引き出しつつもネットワークの安全性も担保するといったものと発想が似ています。

難しいけれど本質を捉えた設計という匂いがしますね。理解してすぐに適用というのは難しいかも知れませんが、これからのネットワーク構築を考える上での一つの参考となりそうです。

ま、まだ 1章しか読んでないけどね。。

Goの並列化パターン - Context

課題

goroutine 開始後に処理を中断したいケースがある。全てのコールスタックに doneチャネルを連携すればこの実現はできるが、以下の様な操作は実現できない

  • 一部のコールスタックを即時中断
  • 一部のコールスタックにタイムアウトを設定
  • 一部のコールスタックに限定した情報管理

解決

Go 標準の context.Context を活用し中断処理を実現する。Context が提供する機能は以下の通り

  • 各種中断処理
  • Contextスコープの提供 (key-value形式のデータ)

また、context.Context は設計を通じて以下の価値も提供している

  • スコープを分割した並列操作

    • 同じContextを引き回せば同スコープ
    • 別のContextを生成すれば別スコープ
  • Context更新による上位スタックへの影響排除 (親 Contextを元に子 Contextを生成した場合)

    • 親ContextへのCancel操作は 子Contextに影響あり (Doneとなる)
    • 子ContextへのCancel操作は 親Contextに影響なし (Doneとならない)

実践

以下は context.Contextを用いて 2階層分の関数を呼び出している

  • 0階層 main
  • 1階層 runWithCtx1
  • 2階層 runWithCtx2

1階層目の runWithCtx1main から2並列 (goroutine#0, goroutine#1) で実行されるが、goroutine#1 は実行直後に main からキャンセルされている

2階層目の runWithCtx2 は 1秒間の仕事を5回繰り返す関数だが、この例では上位の runWithCtx1 が設定したタイムアウト 3秒 により終了を待たずにキャンセルされている

package main

import (
    "context"
    "fmt"
    "io"
    "os"
    "strconv"
    "sync"
    "time"
)

func main() {
    fmt.Println("main Started")
    const goroutineNum = 2
    var wg sync.WaitGroup
    wg.Add(goroutineNum)
    for i := 0; i < goroutineNum; i++ {
        ctx, cancel := context.WithCancel(context.Background())
        go func(cxt context.Context, i int) {
            defer wg.Done()
            runWithCtx1(ctx, i)
        }(ctx, i)
        // goroutine #1 の場合のみ直後に cancel指定
        if i == 1 {
            cancel()
        }
    }
    wg.Wait()
    fmt.Println("main Finished")
}

func runWithCtx1(ctx1 context.Context, goroutineNo int) {
    funcName := "runWithCtx1"
    println(os.Stdout, goroutineNo, funcName, "Started")
    work(100 * time.Millisecond)
    if done(ctx1) {
        println(os.Stdout, goroutineNo, funcName, "Cancelled !!")
        return
    }
    // タイムアウトを3秒後に設定
    ctx2, _ := context.WithTimeout(ctx1, 3*time.Second)
    runWithCtx2(ctx2, goroutineNo)

    if err := ctx2.Err(); err != nil {
        println(os.Stdout, goroutineNo, funcName, "Failed because of:"+err.Error())
    }
    println(os.Stdout, goroutineNo, funcName, "Finished")
}

func runWithCtx2(ctx2 context.Context, goroutineNo int) {
    funcName := "runWithContext2"
    println(os.Stdout, goroutineNo, funcName, "Started")
    for i := 0; i < 5; i++ {
        if done(ctx2) {
            println(os.Stdout, goroutineNo, funcName, "Cancelled !!")
            return
        }
        println(os.Stdout, goroutineNo, funcName, "Continue Running "+strconv.Itoa(i))
        work(1 * time.Second)
    }
    println(os.Stdout, goroutineNo, funcName, "Finished")
}

func println(w io.Writer, goroutineNo int, funcName, message string) {
    fmt.Fprintf(w, "# %d %s %s \n", goroutineNo, funcName, message)
}

func done(ctx context.Context) bool {
    select {
    case <-ctx.Done():
        return true
    default:
        return false
    }
}

var work = time.Sleep

実行すると 以下を確認できる

  1. goroutine #1 は実行直後に main 関数で実行したキャンセルにより runWithCtx1 (1層目) でキャンセルされている
  2. goroutine #0runWithCtx1 関数で設定したタイムアウトにより 3秒経過後に runWithCtx2 (2層目) でキャンセルされている
main Started
# 1 runWithCtx1 Started 
# 0 runWithCtx1 Started 
# 1 runWithCtx1 Cancelled !! // *1
# 0 runWithContext2 Started 
# 0 runWithContext2 Continue Running 0 
# 0 runWithContext2 Continue Running 1 
# 0 runWithContext2 Continue Running 2 
# 0 runWithContext2 Cancelled !!  // *2
# 0 runWithCtx1 Failed because of:context deadline exceeded 
# 0 runWithCtx1 Finished 
main Finished

Process finished with exit code 0

まとめ

この様に 各階層で別々のContextを用いることで 細かいコールスタックの制御が可能となっている

  • 1階層目は 並列稼働しているgoroutineを個別に制御できているし
    (これは個別にチャネルを用意すれば Doneチャネルでも実現可能)
  • 2階層目は 1階層目とは別のルールを適用できている
    (これはclone等の機能のない Doneチャネルでは実現が面倒)

こんな感じで良いのかな。間違っていたら教えて下さい。

Golang の並列化パターン - Doneチャネル

課題

goroutine 開始後に処理を中断したいケースがあるが、goroutine 自体に中断機能が無い

解決

チャネル経由で中断リクエストを連携させることで中断機能を実現する

実践

下記は 100の仕事 (work) を 1秒に1個処理する労働者(worker) を表現したコード
中断リクエストを利用して 5秒後に処理キャンセルを行っている

package main

import (
    "fmt"
    "time"
)

type done struct{}
type work struct{}

func main() {
    // 終了通知のチャネルを用意します
    doneChan := make(chan done)

    // 仕事が100個入った仕事のチャネルを用意します
    workChan := createChanWithWorks(100)

    // 1秒に1個ずつ仕事を終わらせられる労働者を用意します
    go runWorker(doneChan, workChan, 1*time.Second)

    // 5秒後にキャンセルします
    time.Sleep(5 * time.Second)
    close(doneChan) // `doneChan <- done{}` でも動作するが、`close(doneChan)` 形式の方がチャネルレベルで状態更新が入る分、例外系の処理フロー(送受信間)に強い

    // main goroutineが先に終わらないよう 3秒待機します
    time.Sleep(3 * time.Second)
}

func createChanWithWorks(numWork int) <-chan work {
    workChan := make(chan work, numWork) // 第2引数はチャネルのサイズ。最初に100の仕事が入る様に拡張
    for i := 0; i < numWork; i++ {
        workChan <- work{}
    }
    fmt.Printf("Create %d works \n", numWork)
    return workChan
}

func runWorker(doneChan <-chan done, workChan <-chan work, processTime time.Duration) {
    for {
        select {
        case <-workChan:
            fmt.Println("Working...")
            time.Sleep(processTime)
        case <-doneChan:
            fmt.Println("Done")
            return
        }
    }
}

実行すると 約5秒後に処理がキャンセルされるのが確認できると思います。

Create 100 works 
Working...
Working...
Working...
Working...
Working...
Done

Process finished with exit code 0

はい、そんな感じでした。

K6

f:id:kooyoo:20191204152956j:plain:w500

概要

今回は 性能測定用の負荷クライアントである K6 を紹介します。 K6golang製の負荷テストツールとなります。特徴はシンプルで強力なところです。

K6はインストール、シナリオ記述、そして実行の各局面でサクッと進められるよう、とてもシンプルな仕様となっています。

性能試験でバタバタしている時に、仮に良いツールがあったとしても学習コストをかけていられないですよね?そんなユーザの心の声を汲んでかK6の習得は簡単です。

インストール

インストールの方法は3種類です。どれも簡単な手順で導入が可能です。

  • パッケージマネージャ
  • シングルバイナリ
  • Docker イメージ

K6CLIのツールなので、Homebrewや apt-get等のパッケージマネージャから落としてくることができます。

$ brew install k6

Mac (Homebrew) の場合これだけです。Linuxの場合は Debian系, RedHat系に応じて インストールすることができます。

また、k6golang製ですので、シングルバイナリのファイルも GitHub上で提供されています。この場合ファイルを落としてきて実行するだけです。簡単ですね。

シナリオ定義

次にシナリオファイルです。シナリオファイルは JSで用意します。こんな感じ。

[basic-test.js]

import http from "k6/http";
import { check, sleep } from "k6";

export default function() {
  http.get("http://test.loadimpact.com");
};

実行

ここまでできたなら、一旦実行してみます。

$ k6 run basic-test.js

1リクエストだけ実行するサンプルができました。次に同じスクリプトを利用して2ユーザによる5秒間, 10rpsでのアクセスを試みます。この指定はスクリプト内での固定定義も可能ですし実行時の引数で動的に指定することも可能です。

$ k6 run -u 2 -d 5s --rps 10 basic-test.js

注意点

注意点として、一定の負荷を超えファイルディスクリプタが枯渇すると実行時にエラーが発生することがあります。 その際は OS上でファイルディスクリプタの最大値をチューニングすると収束します。

結果確認

結果が出力されたら内容を確認してみます。項目数が多いので面くらいますが、ポイントを抑えておけば大丈夫です。 Latencyが気になるなら http_req_duration を、Throughputなら data_received, data_sent を、そもそも全データを処理したかという点なら http_reqsを中心に確認します。

基本メトリクス

# 分類 要素名 内容 観点
1 設定 execution 実行環境 (基本は local) (設定ログ)
2 設定 output 出力先 (設定ログ)
3 設定 script シナリオスクリプト (設定ログ)
4 設定 duration 実行時間 (設定ログ)
5 設定 iterations JSの実行回数 (設定ログ)
6 設定 vus アクティブ仮想ユーザ数 (設定ログ)
7 設定 max 最大可能仮想ユーザ数 (設定ログ)
8 - done プログレスバー -
9 結果 data_received 受信データの総量と秒間データ量 N/Wが輻輳すると秒間転送率が低下
10 結果 data_sent 送信データの総量と秒間データ量 N/Wが輻輳すると秒間転送率が低下
11 結果 checks check関数がある場合の結果 -

HTTP系メトリクス

# 分類 要素名 内容 観点
12 結果 http_reqs k6が生成した総リクエスト数 指定との乖離大は N/W・Server 遅延の可能性
13 結果 http_req_blocked 空きTCPコネクションの待ち時間 TCPの接続プールは充分か
14 結果 http_req_looking_up DNSの名前解決に費やした時間 キャッシュは適切か
15 結果 http_req_tls_handshaking TLSセッションのハンドシェイクに費やした時間 接続におけるTLSの負荷は高いか
16 結果 http_req_sending リモートホストへのデータ送信にかかった時間 送信は遅くないか
17 結果 http_req_waiting リモートホストからの応答待ちにかかった時間 (= TTFB) サーバ処理は遅くないか
18 結果 http_req_receiving リモートホストからのデータ受信にかかった時間 受信は遅くないか
19 結果 http_req_duration リクエスト単体の合計時間 (sending + waiting + receiving) (接続・DNS lookupは含まない) 全体で遅くないか

(Option) レスポンスチェック

性能試験の結果に成功条件を盛り込みたい場合 check 関数を利用して実現することができます。
例えば つぎのスクリプトは 2種類のチェックを行っています。

  • ステータスコードが 200 (成功) であること
  • レスポンスタイムが 3秒 (3000ms) 以内であること
import { check } from "k6";
import http from "k6/http";

export default function() {
  let res = http.get("http://test.loadimpact.com/");
  check(res, {
    "is status 200": (r) => r.status === 200,
    "duration < 3000ms": (r) => r.timings.duration < 3000
  });
}

この様にして実行すると、実行結果上にチェックの結果も追記されます。

この際 ✓ 18 がOKで ✗ 0 がNGを意味します。日本と海外では の意味が逆だったりするので騙されないでください。 また、上記は全件OKですが、NGがある場合はチェック項目ごとに OK数と NG数が表示されます。

✓ status != 4xx
✓ status != 5xx
✗ duration < 3000ms
↳ 99% — ✓ 297 / ✗ 3

参考

まとめ

  • golang 利用で並列実行が強力
  • 環境構築が楽ちん
  • 実シナリオファイルは JavaScript という親切設計

学んできたプログラミング言語 - Java

Java

今まで学んできたプログラミング言語に関して書いてゆきたいと思います。僕が最初に触れたのはJavaです。Javaオブジェクト指向の言語で、当時は比較的新鋭の言語でした。今となっては枯れ切った言語なんですけどね。

もっともその当時の自分はJavaの長所を理解した上でJavaを選択した訳ではありません。会社でJavaを使っていたのでJavaを覚えただけです。主体性のない流れですね。

とはいえJavaは当時から何やら可能性の高そうな言語ではありました。もっとも当時の僕にとって、Javaの長所に関する理解は曖昧です。話には色々と聞くものの実感がありません。

Javaの長所

例えば、Javaの長所と言えば次のような点があります。

マルチプラットフォーム

1つ目のマルチプラットフォームに関する感動は希薄です。何事もそうですが、課題に突き当たる前に解決策を提示されてもその有り難みは理解できないですよね。

事前にマルチプラットフォームではない他の言語でも齧っておけば、あるいはWindow以外のOSと触れ合う機会が多かったら、感動もあったのかも知れませんが、それらに乏しかった自分には「便利なことは聞いています」という程度の認識です。

オブジェクト指向のサポート

さて、暫く使ってみると、Javaはなかなか使い勝手のよい言語です。どの使い勝手が良いのかといえば第二の長所として挙げたオブジェクト指向のあたりです。最初は何の魅力も感じられなかったこの機能ですが、理解し始めるととても強力なものでした。

これの何がすごいって、プログラムの整理が格段と楽になるのです。現実の世界に見立ててプログラムを組み立てるだけですから。プログラムの構造も設計しやすいし、出来上がったコードも直感的です。

ただ難点を言えば、現実の世界を正しく捉えられていればという条件が付きます。当たり前の事ですが、人が違えば同じものも違って見えます。物事を程よい抽象度と適切な観点から観察する力は身につけるのが難しく、それまでは見についていないことにすら気づけないものです。

Javaを通じて学べたこと

この辺になってくると、Javaという言語自体よりもむしろDDDを含めたデザインの話になってくるのですが、注目すべきはJavaにはその分析結果をスッとプログラムに落とせるだけの表現力があるということです。

表現力とはインターフェスや抽象クラスを使った抽象化や、カプセル化や委譲を使った制御も含むのですが、一番重要なのはクラスで対象の状態と振る舞いを定義でき、newするだけでそれをオブジェクトとして量産できるという点です。

デザインパターン

現実を模倣したオブジェクトの活用はそれ自体でも価値あることなのですが、時間が経つとコレを活用してシステムの抽象的な動きまで表現しようという動きが拡がります。いわゆるデザインパターンの出現です。

デザインパターンと言えば C++から生まれた GoFのパターンが有名ですが、Javaを使ってもそれらを表現することができます。業界に入って数年目の頃にはデザインパターンを学んでは試用し、といったことを繰り返していました。

残念ながら今となってはフレームワークや軽量コンテナが強力になり、自分でデザインパターンを駆使しながら基盤プログラムをデザインする必要性や機会も少なくなりましたが、当時は一般的なパターンくらいは知らないとまともな基盤プログラムは作りづらかったわけです。

リファクタリング

次に必要性が出てきたのがリファクタリングです。デザインパターンが主にインターフェイス・クラス単位での最適化だとすれば、こちらは変数・メソッド単位での最適化となります。