AnsibleでAudit


モチベーション

CISベンチマークに沿ったサーバ設定を、、、というところなのだけど、ちょっと目線を変えて、構築「済み」のサーバに対して監査をかけるのが目的。
すでに構築済みの別チームが作った環境に対してセキュリティ監査をかけるというのを考える場合、当然いきなり設定変えるわけには行かないわけで、まずは最初に現在の設定がCISベンチマークどおりになっているかどうかを確認し、なってないところだけ別チームの納得する方法で変更してく、という2段階が必要になるわけで。
そんなわけで、CISの「監査」をAnsibleできれいに実施する方法をあれこれ考えた備忘録です。

Dry Run

素直に考えると、「Dry Run」を使えば?となるわけですが。。。
ansible-playbook -i hosts mysites.yml --check とする。
変更が必要なtaskは"changed"、それ以外は"ok"と表示させたい。
(CISの1項目に対してtaskが1つにはならないので美しくはないが、、一旦それはおいといて)

が、うまく行かないケースがポロポロと。

この方法でうまく行くのはcheck_modeがうまく動くモジュールで監査できる項目に限った話。
モジュールに無い設定で、コマンドを直叩きするしかない項目については、コマンドを走らせないと状況がわからないが、実行結果はかならず「changed」として扱われてしまい、監査上問題ない(変更の必要がない)項目の結果が「ok」になれない、というジレンマになる。
これについて何かうまい方法を考えないと行けないわけです。
他にも監査だけする目的だとうまく合致しないモジュールもちらほらあるし。。

ということで、色々考えてみた雑書きです。
ざっくり方針としては、下記になるかなと。

  1. 監査はAnsibleでは実施しない
  2. 各項目ごとに監査用スクリプト群を作り、factとして一括収集する
  3. Dry Run + 専用モジュール
  4. Dry Run + check_mode対応設定スクリプトタスク
  5. Dry Run + 監査用スクリプトタスク + 設定変更用スクリプトタスク + changeの解釈変更

1.監査はAnsibleでは実施しない

見も蓋もないけど、、一番無難ではあるんですよね。。
別途スクリプトを作って、それを対象のサーバで実施する。
美しいのですが、、、今回はAnsibleで頑張る方法考えるので、なしです。

2.各項目ごとに監査用スクリプト群を作り、factとして一括収集する

これを使います。
https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#variables-discovered-from-systems-facts

項目ごとに監査用のスクリプトを作成し、まず真っ先に対象のサーバのコピーする。

その上で、factとして収集する。

preflight.yml
name: "[PreFlight] - Deploy CIS Audit Facts"
copy:
  directory_mode: yes
  src: facts.d
  dest: "{{ fpath_ansible }}"
  owner: root
  mode: '0700'

name: "[PreFlight] - Gather CIS Audit Facts"
setup:
  fact_path: /etc/ansible/facts.d/ # Default setting, but just in case.

配置するスクリプトは下記な感じ。

audit_X_X_X_X.sh
#!/bin/bash

###############################
# Common Settings
###############################

declare RESULT="" DESCRIPTION=""

output() {
  DESCRIPTION=$(echo -n "${DESCRIPTION}" | sed -z 's/\n/\\n/g' | sed -e "s/['\t' ]\+/ /g")
  cat <<EOF
{
    "result": ${RESULT},
    "desctiption": "${DESCRIPTION}"
}
EOF
  exit 0
}


###############################
# For each audit
###############################

declare STDOUT_MODPROVE="" STDOUT_LSMOD=""

# Check modprove
audit_01() {
  STDOUT_MODPROVE=$(modprobe -n -v <TARGET_FS> 2>&1)

  if [ ! ${?} = '0' ]; then
    RESULT='false'
    DESCRIPTION="modprobe command error, ${STDOUT_MODPROVE}"
    output
  fi

  MODPROVE_SETTING=$(echo "${STDOUT_MODPROVE}" | tail -n1 | sed -e "s/ $//" | sed -e "s/^['\t' ]\+//" | sed -e "s/['\t' ]\+/ /g")
  if [ ! "${MODPROVE_SETTING}" = "install /bin/true" ]; then
    RESULT='false'
    DESCRIPTION="modprobe bad setting, ${STDOUT_MODPROVE}"
    output
  fi

  return
}

# Check module loaded or not
audit_02() {
  STDOUT_LSMOD=$(lsmod | grep <TARGET_FS>)

  if [ ${?} = '0' ]; then
    RESULT='false'
    DESCRIPTION="lsmod bad setting, ${STDOUT_LSMOD}"
    output
  fi

  return
}

監査結果自体はansible_local.<Script Name>.resultに保存される。
監査だけする際は、post_filightのような処理を作って整形して表示設定処理をする。

設定変更もする際のため、別タスクとして、通常モジュールで適切に設定するものを作成する。whenでnot ansible_local.<Script Name>.resultと入れておけば、監査NGのものだけに設定を行う、ということができる。

わかりやすい方針だし、作ったスクリプトは他の用途でも使いまわせるのだけど、、ansibleを実行するたびに全fact監査スクリプトを実行してしまうので重たくなるのが難点。
一括実行のせいで重たくになってしまう点については、下記のように監査スクリプトを都度scriptタスクから呼び出す、という方法を取ることで解決できるが、、、そもそもDry Runできるモジュールで監査もきれいにできる(結果がchangedとしてきれいに評される)ケースもあるわけで、全項目に対してスクリプトを作るというのもやり過ぎな気が。

1_1_1_1.yml
name: "[Audit] 1.1.1.1 - Ensure mounting of cramfs filesystems is disabled"
    script: audit_1.1.1.1.sh # At default path, <role>/files/
    register: audit_1_1_1_1 
    check_mode: no # Run even if "--check" mode.
    changed_when: false
    ignore_errors: yes # Continue if audit failed.

やはりDry Runで美しく完結する方法が捨てがたい。

3.Dry Run + 専用モジュール

自分でモジュール作ってしまう。
Dry Runできれいに動くモジュールで作り込める項目は、通常のモジュールで実装する。

check_mode時の振る舞いもちゃんと実装すればいいだけなのだけど、、
問題としては、コードの可読性が悪くなる。
taskや個別スクリプトを読むだけならまだしも、モジュールまで読まされるのは辛いよなぁ。。

4.Dry Run + check_mode対応設定スクリプト

3の亜流だが、モジュールではなく、別途「check_mode」を解釈できるスクリプトを作っておいて、都度呼び出す。
Dry Runできれいに動くモジュールで作り込める項目は、通常のモジュールで実装する。

スクリプトの実装について、check_modeでの実行時は環境変数として"ansible_check_mode"がtrueになるので、これをsrciptタスクのargなどで渡す。
https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#magic-variables-and-how-to-access-information-about-other-hosts

動作確認はしていないが、監査NG/OKをstdoutに出すようにして、changed_whenでstdoutを評価すれば、おそらく変化が必要なときはchanged、それ以外okにできると思われる。

ただ、もうひとひねりして外部スクリプト使わない方法を取りたい。。

5.Dry Run + 監査用スクリプトタスク + 設定用スクリプトタスク + changeの解釈変更

監査と設定変更の処理を、2つのタスクに分ける方針。(3と4では、モジュールが外部スクリプトが両方の処理を1タスクとして実施できるようにしている。)
下記のような感じ。

1_1_21.yml
name: "1.1.21 - Check sticky bit is set on all world-writable directories"
shell: 
    df --local -P | awk {'if (NR!=1) print $6'} | xargs -I '{}' find '{}' -xdev -type d \( -perm -0002 -a ! -perm -1000 \) 2>/dev/null    register: v_1_1_21_check
changed_when: "v_1_1_21_check.stdout != ''"
check_mode: no # Run even if "--check" mode.

name: "1.1.21 - Ensure sticky bit is set on all world-writable directories"
shell: df --local -P | awk {'if (NR!=1) print $6'} | xargs -I '{}' find '{}' -xdev -type d \( -perm -0002 -a ! -perm -1000 \) 2>/dev/null | xargs chmod a+t
when:
  - "v_1_1_21_check.stdout != ''"

Checkが監査用スクリプト、Ensureが設定用スクリプト。

監査スクリプトでは、監査違反があった際はchangedとして扱うよう、changed_whenで制御する。
設定については、監査違反=不適切な設定があった時だけ実施するのもなので、監査スクリプトのchanged_whenと同じ条件を書いておく。

設定スクリプトは、check_modeの際は実施させない(skip)させる必要がある。
check_modeできれいにモジュールの場合はそもそも監査スクリプトと実行スクリプトに分けたりせず、そのモジュールのDryRunの結果を信じればいい。
check_modeできれいに動かないshellなどのモジュールのときは、必ず結果がchangedになるので、監査側の出力とごっちゃになってわからなくなる。。。

監査+設定のモード(check_modeを有効にしない時)については、監査結果と変更結果の両方がchangedになるので区別がつかないが、、そこはタスクで分けるしか無いかな。。

まとめ

色々考えたが、Ansibleのタスクだけ読めば全貌がわかる、というようにしたいので、5の案をベースに一回組み上げてみようかなと思う。
作ってるうちに方針が変わるかもだけど。