Ansibleでlinux初期ログイン時のパスワード変更をする

[追記] 新しい記事を書きました -> https://blog.kefiwild.com/posts/ansible_initial_ip_password_change/

例えばubuntuで最初にログインするとき、以下のように初期パスワードの変更を求められることがあります。
ラズパイにubuntu 20.04を入れて、初期ユーザとして存在するubuntuユーザでログインしようとしたらこうなります。

WARNING: Your password has expired.
You must change your password now and login again!
Changing password for ubuntu.
Current password:

新たにラズパイにubuntuを入れる等の機会は個人的にはそこそこあります。
Ansibleでこういったラズパイの初期設定をしようとした際に、初期パスワード変更部分でいくつかハマったので書きます。

まずplaybook

だいたいこんな感じになりました

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- name: ping
  ping:
  ignore_unreachable: true
  register: ping_result

- name: check if initial login
  delegate_to: 127.0.0.1
  expect:
    command: ssh {{ ssh_args }} {{ default_user }}@{{ ansible_host }}
    responses:
      (?i)Password: "{{ default_user_password }}"
  register: ssh_result
  changed_when: false
  failed_when:
    - "'You must change your password' not in ssh_result.stdout"
    - ssh_result.rc != 0
  when:
    - ping_result.unreachable is defined
    - ping_result.unreachable == true

- name: change password on initial login
  delegate_to: 127.0.0.1
  expect:
    command: ssh {{ ssh_args }} {{ default_user }}@{{ ansible_host }}
    timeout: 10
    responses:
      "(?i)@.*password": "{{ default_user_password }}"
      "(?i)Current Password": "{{ default_user_password }}"
      "(?i)New password": "{{ tmp_password }}"
  register: change_password_result
  when:
    - ssh_result.skipped is not defined
    - '"You must change your password" in ssh_result.stdout'
    - ping_result.unreachable is defined
    - ping_result.unreachable == true

- name: change ssh password
  set_fact:
    ansible_ssh_pass: "{{ tmp_password }}"
    ansible_become_password: "{{ tmp_password }}"
  when: change_password_result.skipped is not defined

- name: reset connection
  meta: reset_connection

参考: https://github.com/ansible/ansible/issues/1619#issuecomment-445846522

初期パスワード変更が必要かどうかを判定する

初期パスワード変更が必要な状況でAnsibleで接続しようとすると、パスワード変更プロンプトに阻まれて接続失敗となります。

fatal: [test]: UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to create temporary directory.In some cases, you may have been able to authenticate and did not have permissions on the target directory. Consider changing the remote tmp path in ansible.cfg to a path rooted in \"/tmp\", for more error information use -vvv. Failed command was: ( umask 77 && mkdir -p \"` echo ~/.ansible/tmp `\"&& mkdir \"` echo ~/.ansible/tmp/ansible-tmp-1635680250.522707-851912-228594635714524 `\" && echo ansible-tmp-1635680250.522707-851912-228594635714524=\"` echo ~/.ansible/tmp/ansible-tmp-1635680250.522707-851912-228594635714524 `\" ), exited with result 1",
    "skip_reason": "Host test is unreachable",
    "unreachable": true
}

上記のような接続失敗の場合、Ansibleでのタスク結果は unreachable となります(注意: failed でない)。
そこで初期パスワード変更の要否判定として、pingモジュールを使っています。

1
2
3
4
- name: ping
  ping:
  ignore_unreachable: true
  register: ping_result

pingの結果がunreachableならば初期パスワードが必要と判断することにし、ignore_unreachable でエラーを無視することで後続のタスクにつなげます。 また、task実行前にgather_factsが走ってしまいそこで失敗してしまうため、gather_facts をfalseにする必要があります。

expectモジュールを使ってパスワード変更プロンプトに対応する

expectモジュール を使うことで、プロンプトへの入力を自動化できます。
そのままではunreachableのためタスク実行できないため、delegate_to: 127.0.0.1 で、ローカルからsshコマンドを実行します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
- name: change password on initial login
  delegate_to: 127.0.0.1
  expect:
    command: ssh {{ ssh_args }} {{ default_user }}@{{ ansible_host }}
    timeout: 10
    responses:
      "(?i)@.*password": "{{ default_user_password }}"
      "(?i)Current Password": "{{ default_user_password }}"
      "(?i)New password": "{{ tmp_password }}"
  register: change_password_result

パスワード変更後にreset_connectionする

結論から書くと、上記expectモジュールでパスワード変更を行った後に(順序は関係ないかも)、 metaモジュールreset_connection する必要がありました。

1
2
- name: reset connection
  meta: reset_connection

reset_connection しないと、以下のようにパスワード変更直後のssh接続を行うタスクで unreachable となります。

fatal: [test]: UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to create temporary directory.In some cases, you may have been able to authenticate and did not have permissions on the target directory. Consider changing the remote tmp path in ansible.cfg to a path rooted in \"/tmp\", for more error information use -vvv. Failed command was: ( umask 77 && mkdir -p \"` echo ~/.ansible/tmp `\"&& mkdir \"` echo ~/.ansible/tmp/ansible-tmp-1635679566.8478498-610282-252614244168586 `\" && echo ansible-tmp-1635679566.8478498-610282-252614244168586=\"` echo ~/.ansible/tmp/ansible-tmp-1635679566.8478498-610282-252614244168586 `\" ), exited with result 1",
    "unreachable": true
}

とてもハマりました。
このあたりの仕組みはちゃんと分かってないですが、パスワード変更から即座に次のタスクを実行しようとすると、以前の接続が再利用されるような感じ?

初期パスワード変更の状況を再現する

初期パスワード変更のplaybookを書いて試したものの失敗 -> イメージを焼き直してリトライ を何回かやったんですが、結構ハマっちゃったので、初期パスワード変更の状況を再現する方法を調べました。
passwd --expire でユーザパスワードをexpireさせることで、次回ログイン時にパスワード変更プロンプトが出るようになるようです。

$ passwd ubuntu
$ passwd --expire ubuntu
$ chage --list ubuntu

おわりに

playbookは github にあります
あくまで自分が使うためだけを目的にしてるので、もろもろ雑です