はじめに
この記事はNTTテクノクロス Advent Calendar 2022の3日目です。
こんにちは、NTTテクノクロスの原です。初めての社外投稿なので温かい目で読んでいただけたらと思います。
私は普段、社内で他組織の開発チームの技術支援を担当しています。あるプロダクトへ開発メンバとして支援に入っています。開発チームではDevSecOpsで開発しながらセキュリティの観点における品質向上ができるといった考え方が、スピードと品質を両立できることや堅牢なアプリケーション開発につながる考えがありました。そこで社内でDevSecOpsを推進している「セキュリティテスト自動化推進チーム」から提案を受けて、開発しているプロダクトにDevSecOpsを導入することになりました。導入作業を始めてから日も経っていませんが、現場の導入担当としてどのように組み入れていくのか検討した/していることや、悩みを共有することで今後導入を予定している方の参考になればうれしいです。
DevSecOpsとは...DevOpsに対してセキュリティの観点を(開発スピードを損なわないように)組み入れることで、セキュリティを担保しつつ開発するスタイルのこと。近年ではリリース直前などの開発終盤ではなく開発初期段階からセキュリティ観点を入れたテストを実施して、手戻りの少なくする「シフトレフト」といった工程管理の考え方もある。
本来であれば開発初期段階からソフトウェア開発ライフサイクル(SDLC)にセキュリティの観点を組み入れることが望ましいです。しかし、長く開発が続いているプロダクトは途中から導入することになり、開発初期よりも既存環境への影響を考慮するべき点があります。今回はそういった点も含めてどのように導入していったのか紹介できればと思います。
確認・検討事項
DevSecOpsを導入していくには、以下の観点を確認・検討する必要がありました。
- DevOpsがどこまで実現しているのか
- どこに対してセキュリティテスト(DAST)するのか
DASTとは...アプリケーションに対するセキュリティテストの1手法のこと。
「Dynamic Application Security Test(DAST):動的アプリケーションセキュリティテスト」の略で、アプリケーションを動作させた上で、アプリケーションへの侵入をテストしたり、利用しているライブラリなどを特定して脆弱性を発見するテストを指す。
DASTではソースコードにアクセスしてテストは実施されませんがSAST(先頭のSはStatic)といったセキュアではない実装箇所の指摘や利用しているライブラリの脆弱性情報なども含めてテストすることで、ソースコードにセキュリティの穴(脆弱性)を作らないアプローチのテストもある。
DevOpsがどこまで実現しているのか
私が担当しているプロダクトでは、アーキテクチャはマイクロサービスで継続的デプロイはしていません。
どこに対してセキュリティテスト(DAST)するのか
DASTをかけるためにアプローチする方法としては、2パターンが考えられました。
- A. ①の中で自動デプロイを実現して、動作させたアプリケーションに対してテスト
- B. ②のステージング環境に手動でデプロイしたあとにテスト
「A」はマイクロサービスごとにセキュリティテストを実施して、「B」はステージング環境にデプロイしたアプリケーションに対してテストする方法となります。マイクロサービスごとにセキュリティテストを実施して脆弱性を検知することも可能ですが、アプリケーションとしての脆弱性を検知ができないため今回は「B」を選択しました。
環境ツール
DevSecOpsを実現するためのツールは以下を利用します。
- スキャンツール: Docker版OWASP ZAP
- OWASP(The Open Web Application Security Project)の略で、Webアプリケーションにおけるアメリカの非営利団体のこと
- Desktop版、cli版、Docker版の3種類があるが、CI/CDに組み込んで利用する目的なので今回はDocker版を利用する
- 脆弱性管理ツール: Faraday
進め方
直近の方針が決まったところで、環境構築をするためにも実際にOWASP ZAPをかけるために必要な情報や実行方法を確認しながら進めました。
1. ネットワークの疎通確認
2. スキャン方法の検討
3. GitlabのPipelineへ追加
4. 可視化の検討
5. 効果測定と今後の予定
1. ネットワークの疎通確認
CIはGitlab Runnerを利用しているため、まずはrunnerから呼び出されるコンテナからステージング環境に疎通できるか確認しました。
Docker版を利用する場合はDockerネットワークによりホスト間で疎通が確認できていてもコンテナから疎通ができないことがあるため、以下で確認します。
docker run -t owasp/zap2docker-stable curl https://www.example.com
今回の開発環境では、ターゲットのWebアプリケーションへにアクセスする際のDNSが無い環境であったため、docker run
で実行する際に--add-host
オプションを付与して確認しました。
docker run --add-host=example.com:192.0.2.1 -t owasp/zap2docker-stable curl https://www.example.com
今回は幸い疎通可能な環境でしたがコンテナとターゲットが疎通できない場合、別ルートを検討する必要があります。
2. スキャン方法の検討
OWASP ZAPのDocker版によるスキャン方法は代表的に以下の2種類です。(ほかにもいくつかあります)
- Baseline Scan
- Webアプリケーションへ実際の攻撃(不正なリクエストの送信など)は実施しない
- 対象のWebアプリケーションから新しいURLを発見する(短時間で打ち切られる)
- 取得できたHTTPヘッダやJavaScriptへセキュリティテストを実施
- 短時間で終わることと不正なリクエストを送信しないことから本番環境に対しても、頻繁なテストが可能
- Full Scan
- Webアプリケーションへ不正なリクエストなどを送信して攻撃する
- 対象のWebアプリケーションから可能な限り新しいURLを発見する
- 不正なリクエストを送信するため、不要なデータが蓄積される可能性がある
- Baseline Scanと比べると長い時間かかることとデータが蓄積されることから、バックアップ可能である環境に対してテストが可能
本プロダクトでは当初は気軽に実施できることからBaseline Scanを想定していましたが、先述のようにスプリント毎に実施する方針となったため、より脆弱性が検知できるFull Scanを実施する方針となりました。
以下、実行方法です。
docker run -v $(pwd):/zap/wrk/:rw -t owasp/zap2docker-stable zap-full-scan.py \
-t https://www.example.com -n sample.context -U sampleHoge -r testreport.html
対象となるWebアプリケーションでログインが必要ない場合は上記のコマンドによって、指定したターゲットの脆弱性レポートを取得することができます。
しかし、ログインが必要な場合はcontext(ログインするためのファイル)を準備する必要があります。以下が設定例となります。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<configuration>
<context>
<name>example-context</name>
<desc/>
<inscope>true</inscope>
<incregexes>https://www.example.com/.*</incregexes>
<excregexes>https://www.example.com/ignorepage/.*</excregexes>
<tech>
<include>Db</include>
<include>Db.CouchDB</include>
<include>Db.Firebird</include>
<include>Db.HypersonicSQL</include>
<include>Db.IBM DB2</include>
<include>Db.Microsoft Access</include>
<include>Db.Microsoft SQL Server</include>
<include>Db.MongoDB</include>
<include>Db.MySQL</include>
<include>Db.Oracle</include>
<include>Db.PostgreSQL</include>
<include>Db.SAP MaxDB</include>
<include>Db.SQLite</include>
<include>Db.Sybase</include>
<include>Language</include>
<include>Language.ASP</include>
<include>Language.C</include>
<include>Language.JSP/Servlet</include>
<include>Language.Java</include>
<include>Language.Java.Spring</include>
<include>Language.JavaScript</include>
<include>Language.PHP</include>
<include>Language.Python</include>
<include>Language.Ruby</include>
<include>Language.XML</include>
<include>OS</include>
<include>OS.Linux</include>
<include>OS.MacOS</include>
<include>OS.Windows</include>
<include>SCM</include>
<include>SCM.Git</include>
<include>SCM.SVN</include>
<include>WS</include>
<include>WS.Apache</include>
<include>WS.IIS</include>
<include>WS.Tomcat</include>
</tech>
<urlparser>
<class>org.zaproxy.zap.model.StandardParameterParser</class>
<config>{"kvps":"&","kvs":"=","struct":[]}</config>
</urlparser>
<postparser>
<class>org.zaproxy.zap.model.StandardParameterParser</class>
<config>{"kvps":"&","kvs":"=","struct":[]}</config>
</postparser>
<authentication>
<type>2</type>
<strategy>EACH_RESP</strategy>
<pollurl/>
<polldata/>
<pollheaders/>
<pollfreq>60</pollfreq>
<pollunits>REQUESTS</pollunits>
<loggedin>\Q<button type="button" class="Nav__item dropdown-item"><span>LogOut</span></button>\E</loggedin>
<form>
<loginurl>https://www.example.com/api/login</loginurl>
<loginbody>username={%username%}&password={%password%}</loginbody>
<loginpageurl>https://www.example.com/api/login</loginpageurl>
</form>
</authentication>
<users>
<user>1;true;c2FtcGxlSG9nZQo=;2;aG9nZQo=~cGFzc3dvcmQK~</user>
</users>
<forceduser>1</forceduser>
<session>
<type>0</type>
</session>
<authorization>
<type>0</type>
<basic>
<header/>
<body/>
<logic>AND</logic>
<code>-1</code>
</basic>
</authorization>
</context>
</configuration>
Desktop版の場合はContextをGUIから作成し、エクスポートすることができます。
Docker版の場合はcontextを直接作成する必要があります。その場合は以下の部分を設定します。
-
<name>
: 任意のcontext名を記載 -
<incregexes>
: 対象となるURLを記載(正規表現可) -
<excregexes>
: 対象外となるURLを記載(正規表現可) -
<authentication>
: 認証方法-
<type>
: 以下の各認証方式に対応する番号を指定-
Manual Authentication
: 0 -
Form-Based Authentication
: 2 -
JSON-Based Authentication
: 5
-
-
<loggedin>
: ログインした時に表示されるパラメータを記載(設定例ではログアウトボタンがある場合ログイン状態とする) -
<form>
: Form-Basedを選択した場合はログインページやbodyに含むパラメータを指定
-
-
<user>
: ユーザを指定します。設定内容は;(セミコロン)で区切られており、以下の順番で記載-
1
: ユーザの設定番号(任意) -
true
: ユーザの有効/無効 -
c2FtcGxlSG9nZQo=
: 設定名をBase64でエンコードした文字列 -
2
: 認証方式 -
aG9nZQo=~cGFzc3dvcmQK~
:ユーザ名とパスワードを指定。パスワードは~(チルダ)で囲んで指定
-
指定するターゲットのURLは対象となるWebアプリケーションの作りによってもスキャン方法が変わります。
対象となるWebアプリケーションがSPAである場合は、URLスキャンができないこともあります。
3. GitlabのPipelineへ追加
- ゆくゆくは自動デプロイを実現した後に脆弱性スキャンを実行できるようにするために、上記のZAPによる脆弱性スキャンをCI/CD Pipelineへ追加していきます。
-
dast
のステージはビルド後に実行されるようになっており、実行後はGitLabからレポートをダウンロードできるようにartifacts
を設定しています。
zap-fullscan:
stage: dast
only: ['tags']
when: manual
variables:
TARGET_URL: https://www.example.com/
TARGET_HOST: www.example.com
TARGET_IP: 192.0.2.1
WORK_DIR: /zap/wrk
before_script:
- cd work
script:
- docker run -u $(id -u ${USER}):$(id -g ${USER}) -v $(pwd)/zap:${WORK_DIR}:rw --rm --add-host=${TARGET_HOST}:${TARGET_IP} \
-t owasp/zap2docker-stable zap-full-scan.py -t "${TARGET_BASE_URL}" -n ${WORK_DIR}/example.context --autooff -r report.html || true
artifacts:
name: "$CI_JOB_NAME"
paths:
- work/zap
4. 可視化の検討
ここからはまだ実現できていませんが、先述の通りfaradayと連携して、脆弱性管理します。
faradayでは公式のDockerが配布されているため、そちらを利用する予定です。
5. 効果測定と今後の予定
- 効果測定としては以下を予定しています。
- 検知した脆弱性内容を精査して、すぐに対応が必要なものは対策
- スプリント毎に実施していき、対応できているのか継続して管理
- 検知した脆弱性内容を精査して、すぐに対応が必要なものは対策
- 今後の予定
- CIに自動ビルドを組み込んで、自動ビルド後にbaselineスキャンを実行
- ゆくゆくは各アクティビティ毎にセキュリティの観点を取り込んでいき、よりセキュアなソフトウェアを継続してリリースできるようにする
- SASTをCIに組み入れてさらに早い段階でソースコードがセキュアな状態になるようにする
- IDEにプラグインを入れることでセキュアなコードをかけるようにする
まとめ
開発が長く続いているプロダクトだと既存環境を踏まえた進め方が必要だったり、管理しているコードやサービスが多いとどのようにアプローチしてDevSecOpsを実現していくのかが悩ましかったです。導入を検討して環境を手をつけ始めてからまだ1〜2週間程度なので、まだまだ道半ばでこれからも検討事項なども出てくると思います。ここまで検討してきた内容などを踏まえて、ほかの皆さんがDevSecOpsを導入するときの参考になれば良いなと思います。
さて、明日(12/4)は@inoue-mnさんがDynamoDBのトランザクションについて記事を投稿される予定です。
残りのNTTテクノクロスのAdvent Calendarを引き続きお楽しみください!