2022年7月30日土曜日

AnsibleでLinuxサーバの初期設定と単体テストを行う

これまで、Ansibleの構築とLinuxサーバへの接続確認まで実施した。今回は、Ansibleを使ってLinuxの初期設定と単体テストを行うPlaybookを作成した。作成する中で、Playbookのテクニックについても知見を得られたので、それも併せて記載する。

Linuxの初期設定とは、例えばsysstattcpdumpなどトラブルシューティング用の定番パッケージのインストール作業や、必要なサービスの起動・不要なサービスの停止作業が該当する。

今までのAnsible関連記事

環境

コントロールノードとなるOSは、AlmaLinuxを用いる。ただし、Red Hat系のディストリビューションであるCentOSやRocky Linuxなどでも同様の手順で構築できるだろう。

  • OS : AlmaLinux 8.5
  • Ansible : ansible [core 2.13.1]

前提として、SELinuxとfirewalldは停止しておく。

# systemctl stop firewalld
# systemctl disable firewalld
# sed -i "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/selinux/config
# reboot

Playbookの処理概要

今回のPlaybookはLinux初期構築を行うものとなるが、大きく「構築パート」と「単体テストパート」に分けて作成している。

構築パートでAnsibleのモジュールを用いてLinuxサーバの各種設定やパッケージのインストールを行う。

単体テストパートでは、構築パートで実施した設定項目と一対一となるように設定確認を行う。しかし本来、Ansibleは冪等性 (処理を何回行っても必ず同じ結果となる性質) があるため、構築パートでAnsibleの処理が成功したことを確認できれば必ず正しい設定が反映されているはずであり、わざわざ単体テストは不要と感じるかもしれない。

ただし、再度設定値確認として単体テストを行うことで、Ansibleでは処理が成功していても何らかの理由で設定が反映されていない可能性を排除できることや、Ansibleの構築処理とは別に設定変更をさせずに単体テストを行うことができるため品質向上を図ることができると考える。

また、基本方針として、単体テストパートの処理を構築パートで利用したモジュールや処理と同じにしてしまうと、万が一設定変更処理自体に問題があった場合に検知ができない可能性があることから、単体テストパートでは別の処理内容 (別のコマンド) にて設定内容を確認をするようにPlaybookを作成した。

Playbookの処理詳細

Playbookのファイルの記載内容は大きく以下のように構成される。

設定項目 説明
hosts 実行対象のサーバを記載する。サーバのリストは別ファイルで作成し、そのファイルの中で実行する対象のサーバやグループを指定する。
vars 変数を記載する。ここで記載した変数は{{ 変数名 }}という形で参照できる。また、変数は配列して複数の要素を持たせることもできる。
tasks Ansibleで実行するメインの処理を記載する。
handlers Ansibleでメイン処理実行状況に応じて実行する事後処理を記載する。例えば、設定ファイル更新がされた場合 (実行結果がchangedの場合) は設定反映のためにサービス再起動を行うといった使い方をする。今回のPlaybookでは未使用となる。

Ansibleのベストプラクティスでは、これらのファイルを分割して決められたディレクトリに沿って配置するルールとなっているが、今回は入門編ということで、あえて1つのファイルにてPlaybookを作成した。

1. hosts : 実行対象サーバ

実行対象サーバを記載するファイルは、Playbookとは別にhostsというファイル名で作成した。なお、ファイル名はPlaybook実行時に指定できるため、任意の名前で作成して問題ない。

[]でグループ名を記載し、実行対象サーバを羅列する。記載方法はサーバのホスト名を記載し、実際に接続するIPアドレスをansible_hostの変数で記載するとわかりやすい。

hosts

[linux_servers]
server1 ansible_host=192.168.1.101
server2 ansible_host=192.168.1.102
server3 ansible_host=192.168.1.103

2. vars : 変数

Ansibleではvarsに、Playbookで利用する変数を記載する。今回は以下内容を変数として記載した。

変数名 内容
ansible_user Ansibleを実行するための実行対象サーバのユーザ名を記載。
proxy_env dnf実行時にプロキシサーバを指定し、インターネットからパッケージダウンロードをできるようにするため、環境変数http_proxyhttps_proxyを指定する。
install_package_list インストール対象のパッケージを一覧で指定。
disable_service_list 停止するサービスを一覧で指定。
enable_service_list 起動するサービスを一覧で指定。
ntp_server 実行対象に設定するNTPサーバを指定。
nmcli_ipv4_gateway 実行対象サーバに設定するデフォルトゲートウェイを指定。
nmcli_ipv4_dns 実行対象サーバに設定するDNSサーバを指定。
nmcli_ipv4_dns_search 実行対象サーバに設定するDNS検索パスを指定。
---
- name: Setup linux server
  gather_facts: no
  hosts: linux_servers

  vars:
    ansible_user: ansibleuser

    proxy_env:
      http_proxy: 192.168.1.1:8080
      https_proxy: 192.168.1.1:8080

    install_package_list:
      - open-vm-tools
      - rsyslog
      - chrony
      - sos
      - sysstat
      - tcpdump
      - cifs-utils
      - nfs-utils
      - bind-utils

    disable_service_list:
      - firewalld

    enable_service_list:
      - chronyd

    ntp_server: 192.168.1.2

    nmcli_ipv4_gateway: 192.168.1.254
    nmcli_ipv4_dns: "192.168.1.3 192.168.1.4"
    nmcli_ipv4_dns_search: test.local

~(以下略)~

3. tasks : 処理 (構築パート)

Playbookの処理はtasksに記載する。まず、実行対象サーバに設定を投入する構築パートの処理内容を以下に記載する。
※モジュール名はFQCNで記載すると長くなるため名前だけの記載にしているが、PlaybookにはFQCNで記載することを推奨する。

処理内容 モジュール 説明
タイムゾーン設定 timezone Asia/Tokyoに設定。
インタフェース設定 command nmcliコマンドを実行し、デフォルトゲートウェイ、DNSサーバ、DNS検索パスの設定を行う。設定反映のため、nmcliコマンドでインタフェースのUpを行う。なお、community.general.nmcliでも各種設定が可能ではあるが、私の環境では想定通りの動作がしないことがあったり、最終的に設定反映のためのインタフェースのUpは直接nmcliコマンドで実行する必要があることから、すべての設定作業をnmcliコマンドで直接実行する形式としている。
パッケージインストール dnf 変数install_package_listにて指定した複数のパッケージをdnfを用いてインストールする。その際にプロキシサーバ経由でインターネットアクセスができるよう環境変数http_proxyhttps_proxyを指定する。
サービス無効化service 変数disable_package_listで指定した複数のサービスを無効化する。
サービス有効化 service 変数enable_package_listで指定した複数のサービスを有効化する。
SELinux無効化selinux SELinuxを無効化し、再起動が必要な場合はOS再起動を行い設定を反映する。
時刻同期設定 (Chrony設定)lineinfile ChronyのNTP同期先を1つ設定する。具体的には、デフォルトで存在するserver句またはpool句を指定したserver句に置換する。設定後、Chronyのサービス再起動を行い設定を反映する。

setup_linux_server.yml (抜粋)

---
- name: Setup linux server
  gather_facts: no
  hosts: linux_servers

  vars:
   ~(中略)~
  tasks:
    # Build
    - name: Set timezone
      community.general.timezone:
        name: Asia/Tokyo
      become: true

    - name: Set network interface
      ansible.builtin.command: nmcli c mod ens192 {{ item }}
      loop:
        - connection.autoconnect yes
        - ipv4.gateway "{{ nmcli_ipv4_gateway }}"
        - ipv4.dns "{{ nmcli_ipv4_dns }}"
        - ipv4.dns-search "{{ nmcli_ipv4_dns_search }}"
      changed_when: false
      become: true

    - name: Restart network interface
      ansible.builtin.command: nmcli c up ens192
      changed_when: false
      become: true

    - name: Install packages
      ansible.builtin.dnf:
        name: "{{ package_install_list }}"
        state: present
      become: true
      environment: "{{ proxy_env }}"

    - name: Disable service
      ansible.builtin.service: name={{ item }} enabled=no state=stopped
      loop: "{{ service_disable_list }}"
      become: true

    - name: Enable service
      ansible.builtin.service: name={{ item }} enabled=yes state=started
      loop: "{{ service_enable_list }}"
      become: true

    - name: Disable SELinux
      ansible.posix.selinux:
        state: disabled
      become: true
      register: result

    - name: Reboot (when reboot required)
      ansible.builtin.reboot:
      async: 1
      poll: 0
      changed_when: false
      become: true
      when: result.reboot_required

    - name: Wait for reboot
      ansible.builtin.wait_for_connection:
        delay: 5
        timeout: 120
      when: result.reboot_required

    - name: Set chrony
      ansible.builtin.lineinfile:
        path: /etc/chrony.conf
        regexp: '^(pool|server)'
        line: 'server {{ ntp_server }} iburst'
      become: true

    - name: Restart chrony
      ansible.builtin.service: name=chronyd state=restarted
      become: true
      changed_when: false

~(以下略)~

4. tasks : 処理 (単体テストパート)

引き続きtasksの中の単体テストパートを記載する。
※モジュール名はFQCNで記載すると長くなるため名前だけの記載にしているが、PlaybookにはFQCNで記載することを推奨する。


処理内容 モジュール 説明
タイムゾーン確認 shell Asia/Tokyoに設定されていることをコマンドにて確認。
インタフェース設定確認 shell nmcliコマンドを実行し、デフォルトゲートウェイ、DNSサーバ、DNS検索パスの設定確認を行う。
パッケージインストール確認 shell 変数install_package_listにて指定した複数のパッケージをrpm -qコマンドを用いてインストールされていることを確認する。
サービス無効化確認 shell 変数disable_package_listで指定した複数のサービスをsystemctl is-enabledコマンドを用いて無効化されていることを確認する。なお、systemctlはサービスがdisabledの場合、返り値が1となるため、set -o pipefailは付与しない。
サービス有効化確認 shell 変数disable_package_listで指定した複数のサービスをsystemctl is-enabledコマンドを用いて有効化されていることを確認する。なお、systemctlはサービスがdisabledの場合、返り値が1となるため、set -o pipefailは付与しない。
SELinux確認 command SELinuxが無効化されていることをコマンドにて確認する。
時刻同期設定確認 (Chrony設定確認) shell ChronyのNTP同期先をに対して時刻同期が成功していることを確認する。

setup_linux_server.yml (抜粋)

---
- name: Setup linux server
  gather_facts: no
  hosts: linux_servers

  vars:
   ~(中略)~
  tasks:
   ~(中略)~

    # Unit test
    - name: Test timezone
      ansible.builtin.shell: set -o pipefail && timedatectl status | grep 'Asia/Tokyo'
      changed_when: false

    - name: Test network interface
      ansible.builtin.shell: set -o pipefail && nmcli c show ens192 | grep -E {{ item }}
      loop:
        - '^connection.autoconnect.*yes'
        - '^ipv4.addresses.*{{ ansible_host }}'
        - '^ipv4.gateway.*{{ nmcli_ipv4_gateway }}'
        - '^ipv4.dns.*$(echo "{{ nmcli_ipv4_dns }}" | tr " " ",")'
        - '^ipv4.dns-search.*{{ nmcli_ipv4_dns_search }}'
      changed_when: false
      register: result
      environment:
        LANG: C

    - name: Test install packages
      ansible.builtin.shell: rpm -q "{{ item }}"
      loop: "{{ package_install_list }}"
      changed_when: false
      register: result
      tags: skip_ansible_lint

    - name: Test disable service
      ansible.builtin.shell: systemctl is-enabled "{{ item }}" | grep 'disabled'
      loop: "{{ service_disable_list }}"
      changed_when: false
      register: result
      tags: skip_ansible_lint

    - name: Test enable service
      ansible.builtin.shell: systemctl is-enabled "{{ item }}" | grep 'enabled'
      loop: "{{ service_enable_list }}"
      changed_when: false
      register: result
      tags: skip_ansible_lint

    - name: Test SELinux
      ansible.builtin.command: getenforce
      changed_when: false
      register: result
      failed_when: result.stdout != "Disabled"

    - name: Test chrony
      ansible.builtin.shell: set -o pipefail && chronyc -n sources | grep -E '\^\* {{ ntp_server }}' | wc -l
      changed_when: false
      register: result
      until: result.stdout | int == 1
      retries: 3
      delay: 5

実行結果

実際にPlaybookを実行してみよう。failed=0であれば、問題なく処理が成功している。

# ansible-playbook -i hosts -l server1 setup_linux_server.yml
PLAY [Setup linux server] ***************************************************************************************************

TASK [Set timezone] *********************************************************************************************************
changed: [server1]

TASK [Set network interface] ************************************************************************************************
ok: [server1] => (item=connection.autoconnect yes)
ok: [server1] => (item=ipv4.gateway "192.168.1.254")
ok: [server1] => (item=ipv4.dns "192.168.1.4")
ok: [server1] => (item=ipv4.dns-search "test.local")

TASK [Restart network interface] ********************************************************************************************
ok: [server1]

TASK [Install packages] *****************************************************************************************************
changed: [server1]

TASK [Disable service] ******************************************************************************************************
changed: [server1] => (item=firewalld)

TASK [Enable service] *******************************************************************************************************
ok: [server1] => (item=chronyd)

TASK [Disable SELinux] ******************************************************************************************************
[WARNING]: SELinux state temporarily changed from 'enforcing' to 'permissive'. State change will take effect next reboot.
changed: [server1]

TASK [Reboot (when reboot required)] ****************************************************************************************
ok: [server1]

TASK [Wait for reboot] ******************************************************************************************************
ok: [server1]

TASK [Set chrony] ***********************************************************************************************************
changed: [server1]

TASK [Restart chrony] *******************************************************************************************************
ok: [server1]

TASK [Test timezone] ********************************************************************************************************
ok: [server1]

TASK [Test network interface] ***********************************************************************************************
ok: [server1] => (item=^connection.autoconnect.*yes)
ok: [server1] => (item=^ipv4.addresses.*192.168.1.101)
ok: [server1] => (item=^ipv4.gateway.*192.168.1.254)
ok: [server1] => (item=^ipv4.dns.*$(echo "192.168.1.4" | tr " " ","))
ok: [server1] => (item=^ipv4.dns-search.*test.local)

TASK [Test SELinux] *********************************************************************************************************
ok: [server1]

TASK [Test install packages] ************************************************************************************************
ok: [server1] => (item=open-vm-tools)
ok: [server1] => (item=rsyslog)
ok: [server1] => (item=chrony)
ok: [server1] => (item=sos)
ok: [server1] => (item=sysstat)
ok: [server1] => (item=tcpdump)
ok: [server1] => (item=cifs-utils)
ok: [server1] => (item=nfs-utils)
ok: [server1] => (item=bind-utils)

TASK [Test disable service] *************************************************************************************************
ok: [server1] => (item=firewalld)

TASK [Test enable service] **************************************************************************************************
ok: [server1] => (item=chronyd)

TASK [Test chrony] **********************************************************************************************************
ok: [server1]

PLAY RECAP ******************************************************************************************************************
server1                  : ok=18   changed=5    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

以上で、AnsibleによるLinuxの初期設定と単体テストを実現することができた。今回は1つのPlaybookにてすべての処理を実行したが、前述したとおりAnsibleのベストプラクティスでは、varsやtasksなどのファイルを分割してPlaybookを構成する。

次回は、Ansibleのベストプラクティスのディレクトリ構成にてPlaybookを実行してみたいと思う。

2022年7月23日土曜日

AnsibleからLinuxサーバに接続するための事前作業

前回の記事にて、Ansibleコントロールノードを構築手順を記載した。今までと、これから執筆予定のAnsible関連記事は以下となる。

本記事では第二弾として、AnsibleからLinuxサーバに接続し処理を実行できるようにするための事前作業を記載する。なお、Ansibleはエージェントレスであり、ほとんどの手順は、SSH接続のために必要な手順を実施すればよく、特殊な手順はほとんどない。

また、SSH接続には公開鍵認証方式とパスワード認証方式の2つの手法がある。本記事では両方の手順について記載する。

認証方式 説明
公開鍵認証方式 SSHクライアント (Ansibleコントロールノード) で作成した公開鍵をSSHサーバ (実行対象サーバ) に信頼できるものとして事前に登録しておき、認証時に送られてる署名情報を復号して検証することで認証を行う方式。
パスワード認証方式 ユーザ、パスワードによる認証を行う方式。

環境

コントロールノード及び実行対象サーバのLinuxのOSは、AlmaLinuxにて構築している。ただし、Red Hat系のディストリビューションであるCentOSやRocky Linuxなどでも同様の手順で対応できるだろう。

  • コントロールノード
    • OS : AlmaLinux 8.5
    • Ansible : ansible [core 2.13.1]
  • Ansible実行対象サーバ
    • OS : AlmaLinux 8.5
    • IPアドレス : 192.168.11.193

AnsibleによるLinuxサーバ接続手順 (公開鍵認証)

1. Ansible実行用ユーザを作成しパスワードを設定

実行対象サーバにてAnsible実行用ユーザを作成する。今回はansibleuserという名前のユーザを作成した。

また、公開鍵認証で接続するユーザとなるが、初回の鍵登録時にSSH接続が必要となるので、任意のパスワードを設定しておく。本ユーザはsudoで特権コマンドの実行が可能となるため、強固なパスワードを設定することをお勧めする。

[実行対象サーバ]
# useradd -u 2001 ansibleuser
# echo 'ansibleuser:[任意のパスワード]' | chpasswd

2. sudo設定を追加

Ansible実行ユーザで特権コマンドを実行できるよう、sudoの設定を追加する。

[実行対象サーバ]
# echo 'ansibleuser ALL=(root) NOPASSWD:ALL' >> /etc/sudoers
# visudo -c
/etc/sudoers: 正しく構文解析されました

4. Ansibleコントロールノードより公開鍵をコピー

SSHクライアントとなるAnsibleコントロールノードにて、秘密鍵と公開鍵の作成を行う。いくつか確認を求められるが、すべてエンターを押しておけば問題ない。

[Ansibleコントロールノード]
# ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): ★そのままエンター
Created directory '/root/.ssh'.
Enter passphrase (empty for no passphrase): ★そのままエンター
Enter same passphrase again: ★そのままエンター
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:ka6cvxU3D8QZZVYg3xRYfXWfL5sKYRDo4EZv+Zu/leU root@ansible
The key's randomart image is:
+---[RSA 3072]----+
|       .o.=*=. oo|

~(中略)~

|        ++=o.    |
+----[SHA256]-----+

作成した公開鍵をssh-copy-idコマンドで実行対象サーバにコピーする。

[Ansibleコントロールノード]
# ssh-copy-id -i ~/.ssh/id_rsa.pub ansibleuser@192.168.11.193

5. Ansibleコントロールノードからの接続確認

最後に動作確認として、Ansibleのpingモジュールを用いて接続確認を行う。インベントリファイルと呼ばれる実行対象サーバの情報を記載したファイルをhostsという名前で作成し、ansibleコマンドを用いてpingモジュールを実行する。

特にパスワードを入力を求められることなく、pongが返ってくればOKとなる。

[Ansibleコントロールノード]
# cd ~
# echo "192.168.11.193" > hosts
# ansible -i hosts -u ansibleuser -m ping 192.168.11.193
192.168.11.193 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "changed": false,
    "ping": "pong"   ★pongが返ってくればOK
}

AnsibleによるLinuxサーバ接続手順 (パスワード認証)

1. Ansible実行用ユーザを作成しパスワードを設定

実行対象サーバにてAnsible実行用ユーザを作成する。今回はansibleuserという名前のユーザを作成した。

[実行対象サーバ]
# useradd -u 2001 ansibleuser
# echo 'ansibleuser:[任意のパスワード]' | chpasswd

2. Ansible実行用ユーザをwheelグループに追加

sudo実行時もパスワード認証にて行うようにできるため、今回はsudoを設定するのではなく、もともと用意されているwheelグループに所属させる方針とする。

[実行対象サーバ]
# usermod -aG wheel ansibleuser

3. Ansibleコントロールノードからの接続確認

最後に動作確認として、Ansibleのpingモジュールを用いて接続確認を行う。

パスワードを入力が必要となるため、--ask-passオプションを付与して実行する。正しいパスワードを入力することでpingモジュールが実行され、pongが返ってくればOKとなる。

[Ansibleコントロールノード]
# cd ~
# echo "192.168.11.193" > hosts
# ansible -i hosts -u ansibleuser -m ping 192.168.11.193 --ask-pass
SSH password:  ★ansibleuserのパスワードを入力
192.168.11.193 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "changed": false,
    "ping": "pong"   ★pongが返ってくればOK
}

ただし、毎回パスワードを入力することは手間となる。パスワード入力を回避したい場合は、インベントリファイルにansible_passwordの変数を設定しパスワードを登録しておけば、パスワードを入力を省略できる。
※(2022/7/23修正) ansible_ssh_passansible_passwordに修正。

# echo "192.168.11.193 ansible_password=[ansibleuserのパスワード]" > hosts
# ansible -i hosts -u ansibleuser -m ping 192.168.11.193
192.168.11.193 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "changed": false,
    "ping": "pong"   ★pongが返ってくればOK
}

ただし、この方法ではパスワードが平文でテキストファイルに書かれてしまうという問題があり、セキュリティリスクがある。パスワードを平文でインベントリファイルやPlaybookに記載しないためのツールとして、「Ansible Vault」があるが、こちらについては別記事で使用方法を紹介することとしたい。

以上で、AnsibleからLinuxサーバに接続するための事前作業は完了となる。次回以降の記事で、実際にLinuxサーバに対してPlaybookを実行してみたいと思う。

参考

更新履歴

  • 2022/7/23 ansible_ssh_passansible_passwordに修正。
2022年7月16日土曜日

AnsibleコントロールノードをAlmaLinux上に構築する

Ansibleは以前からきちんと使えるようになりたいと思いつつ、業務が多忙だったことや、他に優先して勉強することがあるなどで、きちんと習得できていなかった。

ただ、いくら業務で使っていないからといって、いつまでもAnsibleを避けていては、従来のインフラの構築手法や知識からの発展が見込めないため、ようやく腰を据えてAnsibleを勉強しなおすことにした。

具体的には、自宅の検証環境にAnsibleコントロールノードを構築し、Linuxサーバ、Windowsサーバ、VMware仮想環境などの構成管理をAnsibleにて実現したいと考えている。

実現結果は本ブログで記事として順次公開していきたい。以下、今後記事として公開したい内容となる。

まず第一弾として、本記事ではAnsibleコントロールノードをAlmaLinux上に構築する手順を記載する。

環境

コントロールノードとなるOSは、AlmaLinuxを用いる。ただし、Red Hat系のディストリビューションであるCentOSやRocky Linuxなどでも同様の手順で構築できるだろう。

  • OS : AlmaLinux 8.5
  • Ansible : ansible [core 2.13.1]

前提として、SELinuxとfirewalldは停止しておく。

# systemctl stop firewalld
# systemctl disable firewalld
# sed -i "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/selinux/config
# reboot

Ansibleコントロールノード構築手順

1. dnfでpipをインストール

Ansibleはdnfで直接パッケージをインストールせず、Python用のパッケージ管理ツールである「pip」を用いる。pip自体はdnfでインストールできる。

また、AnsibleからLinuxに接続する際にSSH公開鍵認証方式ではなく、パスワード認証による接続をできるようにするため、sshpassのインストールも併せて行う。

# dnf install epel-release python3-pip sshpass -y

2. Python3をバージョンアップ

最新版のAnsibleを利用する場合、Pythonも新しいバージョンを使用する必要がある。例えば、最近のバージョンでは以下のバージョン制約がある。

Ansible Python
2.10以降 3.8以降
2.9 3.5以降

AlmaLinuxの場合、パッケージインストールされるバージョンは以下の通りとなる。

# python3 --version
Python 3.6.8

バージョンを最新化するため、dnfを用いてpython38をインストールする。

# dnf install python38 -y

通常利用するPythonをPython 3.6から3.8に変更するため、alternativesを使って、以下の通り設定する。

# alternatives --list
libnssckbi.so.x86_64    auto    /usr/lib64/pkcs11/p11-kit-trust.so
python                  auto    /usr/libexec/no-python
ifup                    auto    /usr/libexec/nm-ifup
cifs-idmap-plugin       auto    /usr/lib64/cifs-utils/cifs_idmap_sss.so
python3                 auto    /usr/bin/python3.6
# alternatives --set python3 /usr/bin/python3.8
# alternatives --list
libnssckbi.so.x86_64    auto    /usr/lib64/pkcs11/p11-kit-trust.so
python                  auto    /usr/libexec/no-python
ifup                    auto    /usr/libexec/nm-ifup
cifs-idmap-plugin       auto    /usr/lib64/cifs-utils/cifs_idmap_sss.so
python3                 manual  /usr/bin/python3.8

3. pipをバージョンアップ

Ansibleインストール前に、pip自体をバージョンアップする。

# pip3 install --upgrade pip
WARNING: Running pip install with root privileges is generally not a good idea. Try `pip3 install --user` instead.
Collecting pip
  Downloading https://files.pythonhosted.org/packages/96/2f/caec18213f6a67852f6997fb0673ae08d2e93d1b81573edb93ba4ef06970/pip-22.1.2-py3-none-any.whl (2.1MB)
     |????????????????????????????????| 2.2MB 14.1MB/s
Installing collected packages: pip
Successfully installed pip-22.1.2

4. Ansibleをインストール

pipにてAnsibleをインストールする。なお、特にバージョン指定を行わない限り、最新バージョンのAnsibleが導入される。

# pip3 install ansible
WARNING: pip is being invoked by an old script wrapper. This will fail in a future version of pip.
Please see https://github.com/pypa/pip/issues/5599 for advice on fixing the underlying issue.
To avoid this problem you can invoke Python with '-m pip' instead of running pip directly.
Collecting ansible
  Downloading ansible-6.0.0-py3-none-any.whl (40.3 MB)
     qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq 40.3/40.3 MB 32.1 MB/s eta 0:00:00
Collecting ansible-core~=2.13.0
  Downloading ansible_core-2.13.1-py3-none-any.whl (2.1 MB)
     qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq 2.1/2.1 MB 33.7 MB/s eta 0:00:00

~(中略)~

Installing collected packages: resolvelib, PyYAML, pyparsing, pycparser, MarkupSafe, packaging, jinja2, cffi, cryptography, ansible-core, ansible
Successfully installed MarkupSafe-2.1.1 PyYAML-6.0 ansible-6.0.0 ansible-core-2.13.1 cffi-1.15.1 cryptography-37.0.2 jinja2-3.1.2 packaging-21.3 pycparser-2.21 pyparsing-3.0.9 resolvelib-0.8.1
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv

インストールされたAnsibleのバージョンは以下の通り。2.13.1という最新バージョン (2022年7月時点) が導入されている。

# ansible --version
ansible [core 2.13.1]
  config file = None
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python3.8/site-packages/ansible
  ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/local/bin/ansible
  python version = 3.8.12 (default, Apr 21 2022, 07:55:08) [GCC 8.5.0 20210514 (Red Hat 8.5.0-10)]
  jinja version = 3.1.2
  libyaml = True

なお、何らかの理由で古いバージョンのAnsibleを使用したい場合は、以下の通りバージョン指定を行うことでインストールできる。以下はバージョン2.9をインストールする場合の例となる。

# pip3 install "ansible==2.9.*"
# ansible --version
ansible 2.9.27
  config file = None
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python3.8/site-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 3.8.12 (default, Apr 21 2022, 07:55:08) [GCC 8.5.0 20210514 (Red Hat 8.5.0-10)]

以上で、Ansibleコントロールノードとして必要な作業は完了となる。次は、実際にAnsibleにて接続確認まで実施してみよう。

自身のOSに対してAnsibleによる接続確認

1. Ansible実行ユーザを作成

Ansible実行のユーザを作成する。今回はansibleuserという名前で作成した。Ansible実行ユーザは、SSHで接続できるようパスワード設定を行い、特権が必要な処理を実行するためにsudoできるよう、/etc/sudoersに設定を行う。

# useradd -u 2001 ansibleuser
# echo 'ansibleuser:[任意のパスワードを設定]' | chpasswd
# echo 'ansibleuser ALL=(root) NOPASSWD:ALL' >> /etc/sudoers

2. インベントリファイルの作成

Ansible実行時はインベントリファイルという、実行対象のサーバや接続ユーザ情報などを記載したファイルを指定したうえで実行する。今回は、hostsという名前で以下のようにインベントリファイルを作成した。

# vi hosts
[ansible_server]
192.168.11.25

[ansible_server:vars]
ansible_user=ansibleuser

3. Ansibleによる接続確認

Ansibleの接続確認を行うためのモジュールであるpingモジュールを使用して確認を行う。実行時のオプションの意味は以下の通りとなる。

オプション 説明
-i インベントリファイルを指定。
-m 実行モジュールを指定。
--ask-pass, -k 接続時にパスワード認証を用いる。
# ansible -i hosts -m ping 192.168.11.25 --ask-pass
SSH password: 
192.168.11.25 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "changed": false,
    "ping": "pong"
}

実行結果でpongと帰ってきていれば、Ansible実行に成功している。以上で、Ansibleコントロールノードの構築と接続確認は完了となる。

参考

なお、Ansibleの各種モジュールの使用方法は以下書籍が非常に詳しく参考になる。

2022年7月9日土曜日

Active DirectoryのPSOを作ってパスワードポリシーを設定する

Active Direcotryによるドメインの環境では、グループポリシーを使うことでパスワードのポリシーを設定することができる。

しかし、グループポリシーはOU単位で設定が適用されるため、特定のユーザやセキュリティグループに対してパスワードのポリシーを分ける場合は、OU自体も分ける必要があり、適用先を柔軟に設定することができない。

このような問題を解消する方法として、Active DirecotyではPSO (Password Setting Object) と呼ばれるオブジェクトを作成することで、ユーザやセキュリティグループ単位でパスワードポリシーを適用することができる。

今回、Active DirectoryのPSOを作ってパスワードポリシーを設定する手順を記載する。

環境

Active DirectoryのドメインコントローラーはWindows Server 2016にて確認を行った。

PSOによるパスワードポリシー適用手順

1. Active Directory管理センターを開く

Active DirectoryのドメインコントローラとなっているOSにログインし、「サーバーマネージャー」→「ツール」→「Active Directory管理センター」を選択すると、「Active Directory管理センター」が表示される。

2. Password Settings Containerを開く

左メニューの「ドメイン名」→「System」→「Password Settings Container」を選択する。

PSO未作成の場合は画面右が空欄となっているはずなので、右クリック→「新規」→「パスワードの設定」を選択する。

3. PSOの作成

PSOの作成画面が開くので、セキュリティ要件に従いパスワード要件を設定する。なお、説明欄の記載はあくまで私の見解であり、組織やシステムのセキュリティ要件に従い適切に設定いた抱く必要がある点に注意いただきたい。

設定項目 説明
名前 作成するPSOの任意の名前を設定する。
優先順位 複数のPSOが存在した場合の優先順位を整数で指定する。PSOが1つしかない状況であれば、とりあえず1にしておいてもよいだろう。
パスワードの最小の長さを適用する パスワードの最小文字数の制限をかける設定。デフォルト7文字だが、パスワードの強度を上げる場合は最低でも8文字以上(可能なら14文字以上)で制限をかけるようにしよう。パスワード長は、パスワード長と解読時間の情報などを参考に必要な長さを決定するとよいだろう。
パスワードは要求する複雑さを満たす パスワードの文字列に、英大文字、英小文字、数字、記号を含めるようにする条件。パスワード文字列が十分に長い場合は、本設定は有効にすべきではないとの情報もある (NIST SP800-63-3) が、現実的にはそこまで長いパスワードを設定できないパターンが多いと思われることから、本設定は有効にしたほうがよいのではないかと思う。
パスワードの履歴を記録する 過去に利用したパスワードの再利用を禁止するルール。パスワードの履歴設定および定期的な変更を有効にしても、ユーザはもとのパスワードの文字列を少し変更したものにする傾向にあり、あまり効果的ではないといわれている。なお、通常のグループポリシーは24回が上限であるが、PSOの場合は24以上の値を設定することができる。
暗号化を元に戻せる状態でパスワードを保存する 通常パスワードはパスワード文字列そのものは保存されず、パスワードのハッシュが保存される。同じ文字列であれば同じハッシュ値となることを利用してパスワード認証を行うため、パスワード文字列そのものは保存することは必要とならないため、通常は本設定は無効にすべきだろう。
誤って消されないように保護する 本PSOの設定が削除されるとセキュリティ面で脆弱になることから、右クリックメニューから削除しても「このオブジェクトは保護されています」というエラーを表示し削除できないようにする設定。もし削除したい場合は、一度この設定を無効にしてから、右クリックにて削除する必要がある。
最小パスワード有効期間を適用する パスワード変更後、再変更をできない期間を設定する。
最大パスワード有効期間を適用する パスワードの有効期限を設定する。前述の通り、近年ではパスワードの定期変更は有効な対策にならないことが提唱されているため、設定しないという選択肢もありだろう。
適用するアカウントロックアウトポリシー 悪意のある攻撃者にパスワードの総当たり攻撃などをされた際に、パスワードのロックアウト設定をしておけば防御効果が見込めることから、通常は設定をした方がよいだろう。失敗回数のリセット時間およびロックアウトの解除までの時間は、「分」単位で指定する。

今回は例として、赤枠個所を設定している。

4. PSOの適用先を設定

続けて、適用先のユーザまたはセキュリティグループを設定する。「直接の適用先」から「追加」ボタンを選択し、適用先の情報を指定すればよい。今回はDomain Adminsグループに適用するよう設定した。

5. 設定したPSOの動作確認

設定したPSOが問題なく動作することを確認しよう。リモートデスクトップ接続などで、わざと10回パスワードを間違えてみよう。PSOが適用されていれば、11回目のログイン時に以下のような「ユーザーアカウントはロックされました」といったエラーが表示されるはずだ。

以上で、Active DirectoryのPSOを作ってパスワードポリシーを設定する手順は完了となる。

2022年7月2日土曜日

curlやtelnetを使って添付ファイル付きメールを送信する

メールサーバの構築時などにおいて、作業環境にThunderbirdなどのメールクライアントが導入されておらず、メールの送信確認ができない状況がある。

そのような状況であっても、telnetコマンドを使って直接SMTPのコマンドを実行することでメール送信を行うことができる。なお、近年のLinuxディストリビューションではtelnetコマンドはデフォルトではインストールされていないが、以下のようにcurlコマンドにてtelnetコマンドと同等の処理が可能となる。

curl -v telnet://[接続先IPアドレス]:[ポート番号]

例えば、単純なメールを送るだけであれば、以下のようにTelnetによるコマンド実行をすればメール送信は可能である。

# curl -v telnet://[メールサーバのIPアドレス]:[ポート番号]
helo example.com
mail from: <送信元メールアドレス>
rcpt to: <宛先メールアドレス>
data
From: 送信元メールアドレス
To: 宛先メールアドレス
Subject: 件名

メール本文
.
quit

しかし、メール本文だけではなく添付ファイル付きのメールを送信する場合はもう少しコマンドが長く複雑となる。

本記事ではcurltelnetを使って添付ファイル付きメールを送信する手順を記載する。

環境

本コマンドは、RHEL 7およびRHEL 8の環境で確認しており、問題なく稼働することを確認した。それぞれのcurlのバージョンは以下の通り。

OS curlバージョン
RHEL 7.6 curl 7.29.0
RHEL 8.3 curl 7.61.1

telnetを使ってSMTPで添付ファイル付きメールを送信する手順

1. 添付ファイル付きメールを送信するコマンド

以下コマンドにて添付ファイル付きのメール送信が可能となる。curlコマンドに対して、パイプで複数行にわたるコマンドの実行結果を渡すことで、Telnetで実行したいコマンドを一気に流すことができる。

attachment="添付ファイル名"
from_address="送信元メールアドレス"
to_address="宛先メールアドレス"
subject="メール件名"
message="メール本文"

( echo "helo example.com"
echo "mail from: <${from_address}>"
echo "rcpt to: <${to_address}>"
echo "data"
echo 'Content-Type: multipart/mixed; boundary="ABCDEFGH12345678"'
echo "MIME-Version: 1.0"
echo "From: ${from_address}"
echo "To: ${to_address}"
echo "Subject: ${subject}"
echo ""
echo "--ABCDEFGH12345678"
echo "Content-Type: text/plain; charset=UTF-8; format=flowed"
echo "Content-Transfer-Encoding: 8bit"
echo ""
echo "${message}"
echo ""
echo "--ABCDEFGH12345678"
echo "Content-Type: application/x-zip-compressed; name=\"${attachment}\""
echo "Content-Disposition: attachment; filename=\"${attachment}\""
echo "Content-Transfer-Encoding: base64"
echo ""
echo $(cat "${attachment}" | base64) | tr " " "\\n"
echo ""
echo ""
echo "--ABCDEFGH12345678--"
echo "."
echo "quit"
) | curl -v telnet://[メールサーバのIPアドレス]:[ポート番号]

curlではなくtelnetコマンドを使いたい場合は、最後の行のコマンドをtelnetに変更すれば同様に動作する。

添付ファイルのデータはBase64エンコードし、文字列として記述する必要があるため、base64コマンドにて変換する。ただし、エンコードした文字列をechoで表示すると、改行が失われて空白文字に置換されてしまうので、再度trコマンドに渡して改行コードに置換する処理を入れている。

2. 実行結果

実際の実行結果は以下の通り。250 okが出ていればメールは正常に送信できている。

# attachment="test.zip"
# from_address="from@example1.com"
# to_address="to@example.com"
# subject="Test mail"
# message="Test message."
# 
# ( echo "helo example.com"
> echo "mail from: <${from_address}>"
> echo "rcpt to: <${to_address}>"
> echo "data"
> echo 'Content-Type: multipart/mixed; boundary="ABCDEFGH12345678"'
> echo "MIME-Version: 1.0"
> echo "From: ${from_address}"
> echo "To: ${to_address}"
> echo "Subject: ${subject}"
> echo ""
> echo "--ABCDEFGH12345678"
> echo "Content-Type: text/plain; charset=UTF-8; format=flowed"
> echo "Content-Transfer-Encoding: 8bit"
> echo ""
> echo "${message}"
> echo ""
> echo "--ABCDEFGH12345678"
> echo "Content-Type: application/x-zip-compressed; name=\"${attachment}\""
> echo "Content-Disposition: attachment; filename=\"${attachment}\""
> echo "Content-Transfer-Encoding: base64"
> echo ""
> echo $(cat "${attachment}" | base64) | tr " " "\\n"
> echo ""
> echo ""
> echo "--ABCDEFGH12345678--"
> echo "."
> echo "quit"
> ) | curl -v telnet://192.168.1.1:25
* About to connect() to 192.168.1.1 port 25 (#0)
*   Trying 192.168.1.1...
* Connected to 192.168.1.1 (192.168.1.1) port 25 (#0)
220 mail.example.com ESMTP
250 mail.example.com
250 sender <from@example1.com> ok
250 recipient <to@example.com> ok
354 go ahead
250 ok:  Message 10164 accepted
221 mail.example.com
* Closing connection 0

実際にThunderbirdで確認したメール送信結果は以下の通りとなる。きちんとメールが送信され添付ファイルが付与されていることがわかる。

以上で、curltelnetを使って添付ファイル付きメールを送信する手順は完了となる。

人気の投稿