はじめに
Google Authenticatorによる二段階認証を導入すると、ユーザごとにgoogle-authenticatorコマンドを実行してシークレットキーを作らないと、ログインできなくなる。
二段階認証を導入したので、設定をしていないユーザのログインができないのは当たり前だが、一部のユーザだけ通常のパスワードによるログインを許可したい場合は、二段階認証の設定に追加設定が必要となる。
Google Authenticatorの導入
Google Authenticatorのインストールと設定
作業ミス等により接続不能になることを想定して、作業用のターミナルとは別にssh接続をしておく。
まずはinstallを行う。
$ sudo yum install pam-devel
$ cd /usr/local/src/
$ git clone https://github.com/google/google-authenticator-libpam
$ cd google-authenticator-libpam
$ sudo yum install libtool
$ sudo ./bootstrap.sh
$ sudo ./configure
$ sudo make
$ sudo make install
$ ll /usr/local/lib/security
total 116
-rwxr-xr-x 1 root root 1024 Nov 7 12:05 pam_google_authenticator.la
-rwxr-xr-x 1 root root 112477 Nov 7 12:05 pam_google_authenticator.so
$ sudo cp -pi /usr/local/lib/security/pam_google_authenticator.so /lib64/security/
installが終わったらPAMでGoogle Authenticatorを使う設定を行う。設定ファイルの名前をgoogle-authとし、/etc/pam.dにおく。
$ cat << EOF | sudo tee /etc/pam.d/google-auth
#%PAM-1.0
auth required pam_env.so
auth sufficient pam_google_authenticator.so try_first_pass
auth requisite pam_succeed_if.so uid >= 500 quiet
auth required pam_deny.so
EOF
SSH時の認証でgoogle-authを読み込むように、/etc/pam.d/sshdを設定する。Google Authenticatorによる認証を行い、その次にパスワードによる認証を行い、合わせて二段階認証とするために、パスワード認証設定(auth include password-auth
)の直前にGoogle Authenticatorの認証設定(auth substack google-auth
)を書く。
$ cat /etc/pam.d/sshd
#%PAM-1.0
auth required pam_sepermit.so
auth include password-auth
account required pam_nologin.so
account include password-auth
password include password-auth
# pam_selinux.so close should be the first session rule
session required pam_selinux.so close
session required pam_loginuid.so
# pam_selinux.so open should only be followed by sessions to be executed in the user context
session required pam_selinux.so open env_params
session required pam_namespace.so
session optional pam_keyinit.so force revoke
session include password-auth
$ sudo sed -i '2 a \auth substack google-auth' /etc/pam.d/sshd
sshd_configでChallengeResponse認証を使うように設定する。
$ sudo sed -i 's/ChallengeResponseAuthentication no/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config
$ sudo service sshd restart
公開鍵認証は基本NGとするためにPubKeyAuthentication no
を入れる。例外ユーザも定義できるが、rootは運用中に自分自身にSSHすることもあるだろうから、この例外に追加した方がいいだろう。
PermitRootLogin
はwithout-password
にしたうえで、プライベートIPからアクセスされたときだけ鍵認証できるようにする。
$ sudo sed -i 's/PermitRootLogin .*/PermitRootLogin without-password/' /etc/ssh/sshd_config
$ cat <<EOF | sudo tee -a /etc/ssh/sshd_config
PubKeyAuthentication no
Match User root Address 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
PubKeyAuthentication yes
EOF
$ sudo service sshd restart
ユーザの設定
二段階認証を使うgauthuserとパスワード認証を使うpauthuserがいるとする。
gauthuserについては、google-authenticatorコマンドでシークレットキーを作成する。
$ username=gauthuser
$ sudo su - $username -c "yes | google-authenticator"
$ sudo ls -l /home/$username/.google_authenticator
# secret keyの確認
$ sudo head -1 /home/$username/.google_authenticator
# emergency scratch codesの確認
$ sudo tail -5 /home/$username/.google_authenticator
gauthuserでログインするには、ChallengeResponse認証を指定してログイン試行する。するとOne-time Passwordを聞かれる。その後元々のユーザのパスワードが聞かれる。
pauthuserで通常のパスワードログインしようとすると、パスワードを入力してもエラーとなる。/var/log/secureには以下のようなログが記載される。
Nov 7 12:16:25 myserver sshd(pam_google_authenticator)[22250]: Failed to read "/home/pauthuser/.google_authenticator" for "pauthuser"
Nov 7 12:16:25 myserver sshd(pam_google_authenticator)[22250]: No secret configured for user pauthuser, asking for code anyway.
Nov 7 12:16:25 myserver sshd(pam_google_authenticator)[22250]: Invalid verification code for pauthuser
Nov 7 12:16:26 myserver sshd[22250]: Failed password for pauthuser from XXX.XXX.XXX.XXX port 1313 ssh2
特定のユーザで二段階認証を回避する設定
設定方法
pauthuserが二段階認証ではなく通常のパスワードでログインできるようにするために、/etc/pam.d/google-authの設定に一部追加する。
以下の設定部分で/home/$username/.google_authenticatorを読もうとして、ファイル読み込みに失敗するため、この設定に行く前に/etc/pam.d/google-authの処理を抜けたい。
auth sufficient pam_google_authenticator.so try_first_pass
PAMでは以下の通り処理の継続を制御できる。(参考)
- requisite:処理を継続するには、requiredモジュールが"success(成功)"を返さなければならない。このモジュールが失敗すると、PAMライブラリは"failure(失敗)"を返し、スタック内のほかのモジュールはそれ以上実行されない。
- required:これも必須のモジュールであるが、失敗した場合、PAMのAPIは"failure"を返すものの、同じスタック内のモジュールの実行は継続される。
- sufficient:このモジュールが成功すると、PAMライブラリは、その他のモジュールを実行することなく直ちに"success"を返す。
- optional:このモジュールの成否に関わらず、スタック内のモジュールの実行は継続される。すべてのモジュールの実行が完了した時点でPAMライブラリの成否が確定していない場合、成功したoptionalモジュールが1つでもあれば承認が与えられる
そのため、ログインしようとしているユーザがpauthuserであればgoogle-auth内の処理はそれ以上行わない(つまりsufficient)という記述を、pam_google_authenticator.soの前にしてあげればよい。
pam_succeed_if.soはユーザIDやグループIDなどの条件を指定できる(参考)ため、このモジュールを使って、ユーザ名がpauthuserであるかどうかをチェックする。
pauthuser01, pauthuser02のようにパスワード認証を使うユーザが複数いて、pauthuserから始まるユーザをワイルドカードを使って一括して設定したいとする。
auth sufficient pam_succeed_if.so login =~ pauthuser*
これをpam_google_authenticator.soの直前の行に書くので、汎用的にするなら以下のように/etc/pam.d/google-authを書きたい。
$ users_not_2SV='pauthuser*'
$ cat << EOF | sudo tee /etc/pam.d/google-auth
#%PAM-1.0
auth required pam_env.so
auth sufficient pam_succeed_if.so login =~ $users_not_2SV
auth sufficient pam_google_authenticator.so try_first_pass
auth requisite pam_succeed_if.so uid >= 500 quiet
auth required pam_deny.so
EOF
/etc/pam.d/google-authを書き換えた後に、gauthuserとpauthuserでログインすると、それぞれ二段階認証と普通のパスワード認証でログインできる。
ログで見るPAMの挙動
gauthuserでログインした場合、/var/log/secureには以下のログが記載される。
Nov 7 15:36:27 myserver sshd[24303]: pam_succeed_if(sshd:auth): requirement "login =~ pauthuser*" not met by user "gauthuser"
Nov 7 15:36:33 myserver sshd(pam_google_authenticator)[24303]: Accepted google_authenticator for gauthuser
Nov 7 15:36:44 myserver sshd[24299]: Accepted keyboard-interactive/pam for gauthuser from XXX.XXX.XXX.XXX port 58291 ssh2
Nov 7 15:36:44 myserver sshd[24299]: pam_unix(sshd:session): session opened for user gauthuser by (uid=0)
gauthuserはpauthuser*
の表記に合致しないためpam_succeed_ifは失敗し、次のpam_google_authenticatorに処理が移る。secret keyは設定済みなのでこの処理は成功し、/etc/pam.d/google-authの次の処理、つまりパスワード認証処理に移る。
pauthuserでログインした場合、/var/log/secureには以下のログが記載される。
Nov 7 15:33:54 myserver sshd[24251]: pam_succeed_if(sshd:auth): requirement "login =~ pauthuser*" was met by user "pauthuser"
Nov 7 15:33:54 myserver sshd[24251]: Accepted password for pauthuser from XXX.XXX.XXX.XXX port 8506 ssh2
Nov 7 15:33:54 myserver sshd[24251]: pam_unix(sshd:session): session opened for user pauthuser by (uid=0)
pauthuserはpauthuser*
の表記に合致するためpam_succeed_ifは成功する。sufficientが指定されているので、成功するとその他のモジュールを実行することなく/etc/pam.d/google-auth自体が成功し、次の処理に移る。
つまり、One-time Passwordを聞かれることなく、次のパスワード認証に処理が移行する。
Ansibleでの実装
注意点
- Ansibleの項は2018/4/23の追記となり、従来のCentOS 6ではなくCentOS 7で実行している。
- CentOS 7のデフォルトに合わせて、/etc/pam.d/google-authの一部の値を変更している。
- Ansibleのversionは2.2を使っている。
- Ansibleでは変数名の英訳として二段階認証の Two Step Verification ではなく、二要素認証の 2 Factor Authentification を採用している。
実装のポイント
Ansibleで実装しようとしたとき、yes | google-authenticator
の部分をshellモジュールで実行すると、yes: standard output: Broken pipe\nyes: write error
というエラーがでてしまう。man google-authenticator
で対話式ではなくオプションで実行する方法を確認する必要がある。
Ansibleの機能(ansible_all_ipv4_addresses
)を活かして、Ansibleでログインするのに使用するユーザ(my_ansible_user)も鍵認証できるようにするため、自身のプライベートIPとグローバルIPも追加するようにする。
実装
group_vars/all.yml
two_factor_auth:
users_not_2fa: "pauthuser*"
users:
- username: gauthuser1
uid: 90001
password: YhNhThbK
- username: gauthuser2
uid: 90002
password: s9Nteh6p
playbooks/two_factor_auth.yml
---
- hosts: bastion # 二要素認証を導入するのは踏み台サーバのみ
become: yes
gather_facts: no
roles:
- two_factor_auth
- ssh
roles/two_factor_auth/tasks/main.yml
---
- block:
- name: git clone google-authenticator
tags: two_factor_auth
git:
repo: https://github.com/google/google-authenticator-libpam.git
dest: /usr/local/src/google-authenticator-libpam
update: no
- name: check whether or not installed
tags: two_factor_auth
stat: path=/lib64/security/pam_google_authenticator.so
register: f
- block:
- name: bootstrap
tags: two_factor_auth
shell: cd /usr/local/src/google-authenticator-libpam && ./bootstrap.sh
- name: configure
tags: two_factor_auth
shell: cd /usr/local/src/google-authenticator-libpam && ./configure
- name: make google-authenticator
tags: two_factor_auth
make:
chdir: /usr/local/src/google-authenticator-libpam
- name: make install google-authenticator
tags: two_factor_auth
make:
chdir: /usr/local/src/google-authenticator-libpam
target: install
- name: copy pam_google_authenticator.so
tags: two_factor_auth
copy:
src: /usr/local/lib/security/pam_google_authenticator.so
dest: /lib64/security/pam_google_authenticator.so
remote_src: yes
ignore_errors: True
when: not f.stat.exists
- name: set google-auth pam
tags: two_factor_auth
template:
src: google-auth.j2
dest: /etc/pam.d/google-auth
- name: set google-auth before password-auth in /etc/pam.d/sshd
tags: two_factor_auth
lineinfile:
name: /etc/pam.d/sshd
insertbefore: "^auth .*password-auth"
line: "auth substack google-auth"
backup: yes
- name: create users
tags: two_factor_auth
user:
name: "{{ item.username }}"
uid: "{{ item.uid }}"
password: "{{ item.password | password_hash('sha512') }}"
update_password: on_create
register: user_add
with_items: "{{ (two_factor_auth | default({})).users | default([]) }}"
- name: chage -d 0
tags: two_factor_auth
shell: "chage -d 0 {{ item.item.username }}"
when: item.createhome is defined and item.createhome == true
with_items: "{{ user_add.results }}"
- name: create .google_authenticator
tags: two_factor_auth
shell: |
sudo -u {{ item.username }} /usr/local/bin/google-authenticator -tfd -w17 -r3 -R30 -C
chown {{ item.username }}. /home/{{ item.username }}/.google_authenticator
args:
creates: /home/{{ item.username }}/.google_authenticator
with_items: "{{ (two_factor_auth | default({})).users | default([]) }}"
when: two_factor_auth is defined
roles/two_factor_auth/templates/google-auth.j2
#%PAM-1.0
auth required pam_env.so
{% if two_factor_auth.users_not_2fa is defined %}
auth sufficient pam_succeed_if.so login =~ {{ two_factor_auth.users_not_2fa }}
{% endif %}
auth sufficient pam_google_authenticator.so try_first_pass
auth requisite pam_succeed_if.so uid >= 1000 quiet_success
auth required pam_deny.so
roles/ssh/tasks/main.yml
---
- name: copy sshd_confing
tags: openssh
template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
- name: restart sshd
service: name=sshd enabled=yes state=restarted
roles/ssh/templates/sshd_config.j2
Protocol 2
SyslogFacility AUTHPRIV
PasswordAuthentication yes
{% if 'bastion' in group_names and two_factor_auth is defined %}
ChallengeResponseAuthentication yes
{% else %}
ChallengeResponseAuthentication no
{% endif %}
UsePAM yes
AcceptEnv LANG LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY LC_MESSAGES
AcceptEnv LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE LC_MEASUREMENT
AcceptEnv LC_IDENTIFICATION LC_ALL LANGUAGE
AcceptEnv XMODIFIERS
X11Forwarding yes
PrintLastLog yes
Subsystem sftp /usr/libexec/openssh/sftp-server
PermitRootLogin without-password
{% if 'bastion' in group_names and two_factor_auth is defined %}
PubKeyAuthentication no
Match User root Address 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
PubKeyAuthentication yes
Match User my_ansible_user Address {{ ansible_all_ipv4_addresses | join(',') }}
PubKeyAuthentication yes
{% endif %}