実運用を想定したLinuxユーザ管理の例

# 概要

実運用を想定して、Linux系のOSへのユーザをこんな感じで管理、運用したらよいのではないかという案です。
実際はケースバイケースなのでこれをもとに個々の要件などと照らし合わせて細部の設定を変更していくイメージで書いています。

# ユーザとグループについて

- 各サーバーへのSSH接続は個人毎のアカウントを作成して公開鍵認証方式のみとする。
- 個人アカウントのパスワードは設定するが、主な目的はsudoコマンド実行時に聞かれるパスワードであり、パスワード認証によるSSH接続は禁止する
- 個人ユーザの所属グループは開発者：`developers` or 運用者：`maintainers`の2パターンとする
- システム管理者のみwheelグループをサブグループとして追加する
- グループをまとめると以下の通り。
  - developers：ベンダーなどの開発担当者が所属するグループでログなどの参照のみを可能と想定したグループ
  - maintainers：運用担当者の所属するグループでapplicationsグループに所属するユーザにスイッチができる。
  - applications：ミドルウェアやアプリケーション（Web系/バッチ系）の実行ユーザの所属グループ。自作アプリの場合は主グループ。ミドルウェアでインストール時に独自にユーザやグループを作るタイプ（apache/nginx/mysql/plsql等）場合はサブグループでも可。ポイントとしてはmaintainersグループの所属のアカウントからこのグループに所属するユーザにsu可能とする点。（これが必ずしもベダーではないので、細かいことはサーバーの用途毎にやるべきだと考える）
  - wheel：Redhat系におけるsudoでrootコマンドを実行できるようにするグループ（管理者権限オプションが指定されたユーザのサブグループに指定する）
  - adm：journalctlでログを参照できるようにするためのグループで全員所属させる方針とする。  
    [参考](https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/7/html/system_administrators_guide/s1-using_the_journal#s2-Access_Control)  
方針としてだれでもすべてのログを参照することは許容してしまっているが、特定のログしか見せないようにするにはこのグループに全員所属させるわけではなく、個別の参照できるjournalログを制御する設定が必要になるので、sudo設定のjournalctlのオプション付き設定のコマンドエリアスなどを定義するのかな？

## その他事前準備

- SSH接続からsudoで`useradd`,`usermod`,`userdel`がパスワード無し(:NOPASSWORD)で実行できること

# 管理される側のサーバーの事前準備

## PAMによるsuコマンドの実行を制限する

- wheelグループ以外のスイッチは原則禁止
- 例外としてmaintainersグループに所属するユーザはapplicationsグループの所属ユーザにスイッチできる。

```shell:/etc/pam.d/su
#%PAM-1.0
# rootは全部OK(デフォルト)
auth        sufficient                          pam_rootok.so
# 現在のユーザ（スイッチ前）の所属グループがmaintainersの場合は１行スキップする
auth        [success=1 ignore=1 default=ignore] pam_wheel.so use_uid group=maintainers
# 現在のユーザ（スイッチ前）の所属グループがwheel以外の場合はスイッチ不可のチェック(maintainersグループはここは通らない)
auth        required                            pam_wheel.so use_uid
# 現在のユーザ（スイッチ前）の所属グループがmaintainers以外の場合はスイッチ不可
auth        [success=1 default=ignore]          pam_succeed_if.so use_uid user notingroup maintainers
# maintainersグループもapplicationsグループのユーザにしかスイッチできない
auth        required                            pam_succeed_if.so user ingroup applications
# 以下はデフォルト設定の（はず）
auth        substack                            system-auth
auth        include                             postlogin
account     sufficient                          pam_succeed_if.so uid = 0 use_uid quiet
account     include                             system-auth
password    include                             system-auth
session     include                             system-auth
session     include                             postlogin
session     optional                            pam_xauth.so
```

### 備忘録

デフォルト定義から`auth required pam_wheel.so use_uid`のコメントを外すだけだと、wheelグループ以外のスイッチができなくなってしまう。末尾に"root_only"とすればrootのみのチェックとなるが、逆にrootユーザ以外のスイッチが自由になってします。
→よって、以下のようにpam_wheel.soモジュールの判定をスキップできる条件をpam_succeed_if.soモジュールで行う。

## sudoの設定例

デフォルトでwheelグループはパスワードありでの任意のsudoが実行可能であり、`visudo`などで/etc/sudoersを直接編集する箇所はなかった（はず）なので、/etc/sudoers.d/配下に任意のファイル名で以下の内容のファイルを作成する
ポイントは以下の通り。

- developers/maintainersグループともにsudoでファイルの中身を参照するコマンドなどで/var/log配下のファイルを参照できる  
  ↑のadmグループでも記載したが、すべてのログを見せたくない場合は個別に限定するなどの設定が必要だが、この投稿はあくまもで一例であり、実際にはサーバーの用途毎に設定を変更すべきかもしれない。。。
- DBサーバーの設定例として、maintainersグループのユーザはsudoでDBサーバーの再起動(`systemctl restart mysql.service`etc.)が実行できる。
　他の用途のサーバーであれば、適宜このあたりを可変にするとよいと考える。

```/etc/sudoers.d/hogehoge
Cmnd_Alias LOG_LIST = /usr/bin/ls /var/log/*, /usr/bin/ls * /var/log/*
Cmnd_Alias LOG_VIEW = /usr/bin/cat /var/log/*, /usr/bin/cat * /var/log/*, \
                      /usr/bin/head /var/log/*, /usr/bin/head * /var/log/*, \
                      /usr/bin/tail /var/log/*, /usr/bin/tail * /var/log/*, \
                      /usr/bin/more /var/log/*, /usr/bin/more * /var/log/*, \
                      /usr/bin/less /var/log/*, /usr/bin/less * /var/log/*, \
                      /usr/bin/grep /var/log/*, /usr/bin/grep * /var/log/*, \
                      /usr/bin/egrep /var/log/*, /usr/bin/egrep * /var/log/*
Cmnd_Alias DB_MAINTAINANCE_COMMAND = /usr/bin/mysql*, /usr/bin/systemctl * mariadb, /usr/bin/systemctl * mariadb.service
%developers   ALL=(ALL) LOG_LIST, LOG_VIEW
%maintainers  ALL=(ALL) LOG_LIST, LOG_VIEW, DB_MAINTAINANCE_COMMAND 
```

注意事項してこのファイルのパーミッション設定が必要。
`chmod 0440 /etc/sudoers.d/hogehoge`

# ユーザ管理用シェルの実装例

## ユーザ作成

実行例）`cat /dev/urandom | tr -dc '[:alnum:]' | head -c8 | useradd.sh -a -g maintainers -c "User Name" "userid" "$(cat ~/.ssh/id_rsa.pub)"`

注意事項：公開鍵文字列には空白を含むため、ダブルクォートなどで囲まないとエラーになります。

```shell:useradd.sh
#!/bin/bash

#====================================================
# SSH接続用の個人アカウントを作成するシェル
#  root or パスなしでsudoできるユーザでの実行すること
#====================================================
# usagesに表示する名前です。ここではシェルのファイル名にしています。
PROGRA_NAME="$( basename $0 )"
# 共通関数のロード
. $(dirname $0)/user_common.sh

# ヘルプ表示。（オプションの指定方法に不正があった場合にも出力）
function _usage() {
  cat << __EOF__

Usage:
  ${PROGRA_NAME} [-g|--group developers|maintainers] [-a|--admin] [-c|--comment usercomment] [-h|--help] usrename public_key.
  And input the password used by \`sudo\` from the standard input.

Description:
   Create a personal account for SSH connection.

Options:
  -c --comment  Uesr comments.(=useradd command "c" options.)
  -g --group    Specify the user's group. "developers" or "maintainers". default is "developers".
  -a --admin    Add use to wheel group.(Not specify is false)
  -h --help     Show help message.

__EOF__
}

# オプション値の初期値をここで定義する。
COMMENT=""
MAIN_GROUP=developers
IS_ADMIN=false

# bashの組み込み関数`getopts`はlong nameオプションに対応してないので以下のように引数すべてループしてオプションを解析します。
while [ $# -gt 0 ]; do
  # 現在位置の引数からオプションの名前を抽出（先頭のハイフン1回or2回繰り返しは除去）
  OPT_NAME=$(echo $1 | sed 's/^-\{1,2\}//')
  case "${OPT_NAME}" in
    # ユーザコメントオプション
    'c' | 'comment' )
      COMMENT=$2
      shift 2
      ;;
    # グループ指定オプション
    'g' | 'group' )
      MAIN_GROUP=$2
      shift 2
      ;;
    # wheelグループに所属させるオプション
    'a' | 'admin' )
      IS_ADMIN=true
      shift 1
      ;;
    'h' | 'help' )
      # ヘルプメッセージを表示して即終了。
      _usage
      exit 0
      ;;
    * )
      # ハイフンで始まる場合は不正なオプションとする。（ただし、引数自体をハイフンで始める値が受け付けられない問題があるけど）
      if [[ "$1" =~ ^- ]]; then
        output_error "${PROGRA_NAME}: illegal option -- '${OPT_NAME}'"
        _usage
        exit 1
      fi
      break
      ;;
  esac
done
# パスワードは標準入力から
read -sp  "Creating user's password: " USER_PASSWORD
# 対話型実行の場合は改行されない対策
tty -s && echo ""

#----------------------------------------------------------------
# チェック
#----------------------------------------------------------------
# コマンド（シェル）自体のオプションを除く必須パラメータはユーザ名と公開鍵（文字列）の２つ
if [ $# -ne 2 ]; then
  output_error "Required username & public key(string)!!"
  _usage
  exit 1
fi
USER_NAME=$1
PUBLIC_KEY=$2
# ユーザの書式と存在チェック
_check_user "${USER_NAME}" || exit 1
# 公開鍵書式チェック
_check_publickey "${PUBLIC_KEY}" || exit 1
# パスワードの書式チェック
_check_password "${USER_PASSWORD}" || exit 1
# グループ指定された文字列チェック
_check_group "${MAIN_GROUP}" || exit 1
# ログ参照でjournalctlコマンドを実行できるようにサブグループにadmを指定する
SUBGROUPS="-G adm"
# rootへのスイッチが可能なオプションが指定された場合
if ${IS_ADMIN}; then
  # 管理者は運用担当者のグループでなければならない（事実上、このチェックがなくても弊害はないが紛らわしいので）
  if [ "${MAIN_GROUP}" != "maintainers" ]; then
    output_error "Administorator is must be maintainers group."
    exit 1
  fi
  # wheelグループへも追加する
  SUBGROUPS=" wheel"
fi
#----------------------------------------------------------------
# ユーザ作成＆パスワード設定＆公開鍵のデプロイ
#----------------------------------------------------------------
# "--password"オプションだとなんでかうまく設定されなかったで以下で回避
sudo useradd -g ${MAIN_GROUP} ${SUBGROUPS} -ms /bin/bash -c "${COMMENT}" "${USER_NAME}"
sudo sh -c "echo ${USER_NAME}:${USER_PASSWORD} | chpasswd"
# 公開鍵のデプロイ
sudo mkdir -p /home/${USER_NAME}/.ssh
sudo chmod 0700 /home/${USER_NAME}/.ssh
sudo sh -c "echo ${PUBLIC_KEY} > /home/${USER_NAME}/.ssh/authorized_keys"
sudo chmod 0600 /home/${USER_NAME}/.ssh/authorized_keys
sudo chown -R "${USER_NAME}:${MAIN_GROUP}" /home/${USER_NAME}/.ssh

# 結果出力
output_info "Created user: $(id ${USER_NAME}) / $(egrep "^${USER_NAME}:.+$" /etc/passwd)"

exit 0
```

## ユーザ更新

ユーザ更新は更新したい要素のみをオプション指定します。（パスワードは標準入力からリード）

実行例）`cat /dev/urandom | tr -dc '[:alnum:]' | head -c8 | usermod.sh ―p -r -g developers -c "User Name" -k "$(cat ~/.ssh/id_rsa.pub)" "userid"`

```shell:usermod.sh
#!/bin/bash

#====================================================
# SSH接続用の個人アカウントを更新するシェル
#  root or パスなしでsudoできるユーザでの実行すること
#====================================================
# usagesに表示する名前です。ここではシェルのファイル名にしています。
PROGRA_NAME="$( basename $0 )"
# 共通関数のロード
. $(dirname $0)/user_common.sh

# ヘルプ表示。（オプションの指定方法に不正があった場合にも出力）
function _usage() {
  cat << __EOF__

Usage:
  ${PROGRA_NAME} [-k|--publickey ssh's public_keys(authorized_keys)] [-g|--group developers|maintainers] [-p|--password] [-a|--admin] [-c|--comment usercomment] [-h|--help] usrename.
  And input the password used by \`sudo\` from the standard input.

Description:
   Update a personal account for SSH connection.

Options:
  Specify only the fields to be changed.
  -c --comment  Uesr comments.(=useradd command "c" options.)
  -k --publickey    Update ssh authorized_keys.
  -g --group        Specify the user's group. "developers" or "maintainers".
  -p --password     Update user password(the password used by \`sudo\`). Password input from standard input.
  -a --admin        Add use to wheel group.(Not specify is false)
  -r --revoke-admin Add use to wheel group.(Not specify is false)
  -h --help         Show help message.

Examples:
  Update authrized_keys.
   => ${PROGRA_NAME} -k "ssh-rsa AAAAA...........== foo@hostname" foo

__EOF__
}

# オプション値の初期値をここで定義する。
IS_ADMIN=false
IS_REVOKE_ADMIN=false

# bashの組み込み関数`getopts`はlong nameオプションに対応してないので以下のように引数すべてループしてオプションを解析します。
while [ $# -gt 0 ]; do
  # 現在位置の引数からオプションの名前を抽出（先頭のハイフン1回or2回繰り返しは除去）
  OPT_NAME=$(echo $1 | sed 's/^-\{1,2\}//')
  case "${OPT_NAME}" in
    # ユーザコメントオプション
    'c' | 'comment' )
      COMMENT=$2
      shift 2
      ;;
    # "-k" or "--publickey"の場合(正確には"--k"や"-publickey"も許容するけど。。。)
    'k' | 'publickey' )
      # "-k" or "--publickey"の次の引数をオプションの値として抜粋
      PUBLIC_KEY=$2
      # 公開鍵妥当性チェック
      _check_publickey ${PUBLIC_KEY} || exit 1
      # "-g" or "--group"とその値の分引数配列をシフトさせる。
      shift 2
      ;;
    # "-g" or "--group"の場合(正確には"--o"や"-optvalue"も許容するけど。。。)
    'g' | 'group' )
      # "-g" or "--group"の次の引数をオプションの値として抜粋
      MAIN_GROUP=$2
      # developers or maintainers以外はNG
      _check_group "${MAIN_GROUP}" || exit 1
      # "-g" or "--group"とその値の分引数配列をシフトさせる。
      shift 2
      ;;
    # パスワードの更新
    'p' | 'password' )
      # パスワードの入力自体は標準入力から受ける
      read -sp  "Updating user's password: " USER_PASSWORD
      # 対話型実行の場合は改行されない対策
      tty -s && echo ""
      _check_password ${USER_PASSWORD} || exit 1
      shift 1
      ;;
    # wheelグループにも追加する場合
    'a' | 'admin' )
      IS_ADMIN=true
      shift 1
      ;;
    # wheelグループを削除する場合
    'r' | 'revoke-admin' )
      IS_REVOKE_ADMIN=true
      shift 1
      ;;
    'h' | 'help' )
      # ヘルプメッセージを表示して即終了。
      _usage
      exit 0
      ;;
    * )
      # ハイフンで始まる場合は不正なオプションとする。（ただし、引数自体をハイフンで始める値が受け付けられない問題があるけど）
      if [[ "$1" =~ ^- ]]; then
        output_error "${PROGRA_NAME}: illegal option -- '${OPT_NAME}'"
        _usage
        exit 1
      fi
      break
      ;;
  esac
done

#----------------------------------------------------------------
# チェック
#----------------------------------------------------------------
# コマンド（シェル）自体のオプションを除く必須パラメータはユーザ名と公開鍵（文字列）の２つ
if [ $# -ne 1 ]; then
  output_error "Required username!!"
  _usage
  exit 1
fi
USER_NAME=$1
# 存在チェック＆所属グループが個人ユーザの所属するグループのユーザであるか？
_check_exists_user "${USER_NAME}" || exit 1
# 変更後の主グループ判定
if [ -n ${MAIN_GROUP} ]; then
  CHANGED_MAIN_GROUP=${MAIN_GROUP}
else
  CHANGED_MAIN_GROUP=$(id -gn "${USER_NAME}")
fi
# 変更後にwheelグループに所属させるか判定
CHANGED_ADMIN=false
if ${IS_ADMIN}; then
  # adminの付与を解除オプションが同時に指定された場合
  if ${IS_REVOKE_ADMIN}; then
    output_error "--admin option and --revoke-admin option cannot be specified at the same time."
    exit 1
  fi
  CHANGED_ADMIN=true
elif ! ${IS_REVOKE_ADMIN}; then
  for GRP in $(id -Gn ${USER_NAME}); do
    if [ "${GRP}" == "wheel" ]; then
      CHANGED_ADMIN=true
      break
    fi
  done
fi
# 管理者は運用担当者のグループでなければならない
if ${CHANGED_ADMIN}; then
  if [ "${CHANGED_MAIN_GROUP}" != "maintainers" ]; then
    output_error "Administorator is must be maintainers group."
    exit 1
  fi
fi

#----------------------------------------------------------------
# 更新処理
#----------------------------------------------------------------
# コメント更新オプションが指定された場合
if [ -n "${PUBLIC_KEY}" ]; then
  sudo usermod -c "${COMMENT}" "${USER_NAME}"
fi
# 公開鍵オプションが指定された場合
if [ -n "${PUBLIC_KEY}" ]; then
  # 作成時にこけて再実行することも考慮して作り直しておく
  sudo mkdir -p /home/${USER_NAME}/.ssh
  sudo chmod 0700 /home/${USER_NAME}/.ssh
  sudo sh -c "echo ${PUBLIC_KEY} > /home/${USER_NAME}/.ssh/authorized_keys"
  sudo chmod 0600 /home/${USER_NAME}/.ssh/authorized_keys
  sudo chown -R "${USER_NAME}:${MAIN_GROUP}" /home/${USER_NAME}/.ssh
fi
# パスワードが指定された場合
if [ -n "${USER_PASSWORD}" ]; then
  # パスワード更新
  sudo sh -c "echo ${USER_NAME}:${USER_PASSWORD} | chpasswd"
fi
# 所属グループが指定された場合
if [ -n "${MAIN_GROUP}" ]; then
  sudo usermod -g ${MAIN_GROUP} "${USER_NAME}"
fi
# wheelグループへの追加
if ${IS_ADMIN}; then
  # 既に所属していてもエラーにはならないのでそのまま実行
  sudo gpasswd -a "${USER_NAME}" wheel
fi
# wheelグループからの削除
if ${IS_REVOKE_ADMIN}; then
  # こちらは所属していないのに削除するとエラーになるので判定
  for GRP in $(id -Gn ${USER_NAME}); do
    if [ "${GRP}" == "wheel" ]; then
      sudo gpasswd -d "${USER_NAME}" wheel > /dev/null
      break
    fi
  done
  # もともと所属していなくてもエラーにはしない
fi

# 結果出力
output_info "Updated user: $(id ${USER_NAME}) / $(egrep "^${USER_NAME}:.+$" /etc/passwd)"
exit 0
```

## ユーザを削除

ユーザ削除のオプションは強制モードのみ。

実行例）`userdel.sh -f "userid"`

```shell:userdel.sh
#!/bin/bash
#====================================================
# SSH接続用の個人アカウントを削除するシェル
#  root or パスなしでsudoできるユーザでの実行すること
#====================================================
# usagesに表示する名前です。ここではシェルのファイル名にしています。
PROGRA_NAME="$( basename $0 )"
# 共通関数のロード
. $(dirname $0)/user_common.sh

# ヘルプ表示。（オプションの指定方法に不正があった場合にも出力）
function _usage() {
  cat << __EOF__

Usage:
  ${PROGRA_NAME} usrename.

Description:
   Delete a personal account for SSH connection.

Options:
  -f --force    Ignore error when not exists user.
  -h --help     Show help message.

__EOF__
}

# オプションの初期値定義
# 存在しない場合もエラーにしない（複数サーバー実行時に途中で落ちた場合などの対処用）
IS_FORCE=false

# bashの組み込み関数`getopts`はlong nameオプションに対応してないので以下のように引数すべてループしてオプションを解析します。
while [ $# -gt 0 ]; do
  # 現在位置の引数からオプションの名前を抽出（先頭のハイフン1回or2回繰り返しは除去）
  OPT_NAME=$(echo $1 | sed 's/^-\{1,2\}//')
  case "${OPT_NAME}" in
    'f' | 'force' )
      # 強制指定された場合は存在チェック関数でエラーがあっても"0"正常とする
      IS_FORCE=true
      shift 1
      ;;
    'h' | 'help' )
      # ヘルプメッセージを表示して即終了。
      _usage
      exit 0
      ;;
    * )
      # ハイフンで始まる場合は不正なオプションとする。（ただし、引数自体をハイフンで始める値が受け付けられない問題があるけど）
      if [[ "$1" =~ ^- ]]; then
        output_error "${PROGRA_NAME}: illegal option -- '${OPT_NAME}'"
        _usage
        exit 1
      fi
      break
      ;;
  esac
done

#----------------------------------------------------------------
# チェック
#----------------------------------------------------------------
# コマンド（シェル）自体のオプションを除く必須パラメータはユーザ名の１つ
if [ $# -ne 1 ]; then
  output_error "Required username!!"
  _usage
  exit 1
fi
USER_NAME=$1
# 存在チェック＆所属グループが個人ユーザの所属するグループのユーザであるか？
RESULT=$(_check_exists_user "${USER_NAME}" 2>&1)
if [ $? -ne 0 ]; then
  if ${IS_FORCE}; then
    exit 0
  fi
  output_error "${RESULT}"
  exit 1
fi

#----------------------------------------------------------------
# ユーザを削除
#----------------------------------------------------------------
sudo userdel -r "${USER_NAME}"
if [ $? -ne 0 ]; then
  exit 1
fi
output_info "Deleted user: ${USER_NAME}"
exit 0
```

## 上の３つシェルの共通関数などの定義ファイル

```shell:user_common.sh
#====================================================
# ユーザの作成、変更、削除で利用する共通関数定義
#====================================================
# 個人アカウントの所属しうるグループの定義
GROUP_VALID_VALUES="developers maintainers"

# シアン色でメッセージを標準出力に赤字で出力するための関数。
function output_info() {
  echo -e "\e[36m$@\e[m"
}
# 黄色字で警告メッセージを標準出力に出力するための関数。
function output_warn() {
  echo -e "\e[33m$@\e[m" >&2
}
# 赤字で標準エラー出力にメッセージを出力するための関数。
function output_error() {
  echo -e "\e[31m$@\e[m" >&2
}

# ユーザ名チェック
function _check_user() {
  local USER_NAME=$1
  # 書式チェック
  # - 先頭は英小文字のみ
  # - 使用可能文字は英小文字/数字/ハイフン/アンダースコア/ドット
  # - 先頭は英小文字のみ
  # - 末尾は英小文字or数字
  # - 3文字以上、30文字以下
  local REGEXP_PTN='^[a-z]{1}[-.0-9_a-z]{1,28}[0-9a-z]{1}$'
  if [[ ! "${USER_NAME}" =~ ${REGEXP_PTN} ]]; then
    output_error "Invalid username: '${USER_NAME}'. Regexp pattern: ${REGEXP_PTN}"
    return 1
  fi
  # 既に存在したらNG
  if id ${USER_NAME} > /dev/null 2>&1; then
    output_error "Already exists user: ${USER_NAME}."
    return 1
  fi
}

# オプションで指定、または更新・削除時の既存ユーザのメイングループの妥当性をチェックする
function _check_group_value() {
  local MAIN_GROUP=$1
  for GRP in ${GROUP_VALID_VALUES}; do
    if [ "${MAIN_GROUP}" = "${GRP}" ] ; then
      return 0
    fi
  done
  return 1
}

# グループオプションのチェック
function _check_group() {
  local MAIN_GROUP=$1
  # 空文字でないこと、次のオプションではない(=ハイフンで始まらない)というチェック
  if [[ -z "${MAIN_GROUP}" ]] || [[ "${MAIN_GROUP}" =~ ^-+ ]]; then
    output_error "Group is empty value."
    return 1
  fi
  if ! _check_group_value "${MAIN_GROUP}"; then
    output_error "${MAIN_GROUP} is invalid values. Allowd value is (${GROUP_VALID_VALUES})"
    return 1
  fi
  return 0
}

# 存在チェック＆所属グループが個人ユーザの所属するグループのユーザであるか？（update/delete時）
function _check_exists_user() {
  local USER_NAME=$1
  # local ORIG_MAIN_GROUP=$(id -gn ${USER_NAME} 2>&1)
  # →なぜかlocalをつけるとexit statusが正しく取れないので宣言と分けて書く
  local MAIN_GROUP
  MAIN_GROUP=$(id -gn ${USER_NAME} 2>&1)
  if [ $? -ne 0 ]; then
    output_error "${MAIN_GROUP}"
    return 1
  fi
  # 指定ユーザの所属グループの妥当性チェック
  if ! _check_group_value "${MAIN_GROUP}"; then
    output_error "${USER_NAME} does not belong to the target groups(${GROUP_VALID_VALUES})."
    return 1
  fi
  return 0
}

# sshの公開鍵の妥当性をチェックします
function _check_publickey() {
  # ssh-keygenコマンドを使うのでいったんファイルに吐き出す
  local TMP_PUBLIC_KEY_FILE=/tmp/AUTHORIZED_KEYS_$$
  echo $1 > ${TMP_PUBLIC_KEY_FILE}
  # 上の_check_exists_userと同じでなぜかlocalをつけるとexit statusが正しく取れないので宣言と分けて書く
  local RESULT
  RESULT=$(ssh-keygen -l -f ${TMP_PUBLIC_KEY_FILE} 2>&1)
  local RET=$?
  rm -f ${TMP_PUBLIC_KEY_FILE}
  if [ ${RET} -ne 0 ]; then
    output_error "${RESULT}"
  fi
  return ${RET}
}

# パスワードの妥当性をチェックします
function _check_password() {
  PASSWORD=$1
  # パスワードは8文字以上で英大文字・小文字・数字の１つ以上の組み合わせ
  if [[ ${#PASSWORD} -ge 8 && "${PASSWORD}" == *[A-Z]* && "${PASSWORD}" == *[a-z]* && "${PASSWORD}" == *[0-9]* ]]; then
    return 0
  else
    output_error "Password must be at least 8 characters, And must be contain at least one of uppercase letters, lowercase letters and numbers."
    return 1
  fi
}
```

# 実運用時の想定したフロー

1. 各個人ユーザは秘密鍵と公開鍵のペアを作成する  
`ssh-keygen -t rsa -b 4096 -N '秘密鍵のパスフレーズ' -C 'コメント' -f 出力する秘密鍵のファイルパス` 
2. 管理者にアカウント作成を依頼する
  - ユーザID（３文字以上。30文字いない。英小文字、数字、ドット、ハイフン、アンダースコアのみ。先頭の文字は英子文字のみで末尾に記号は不可。）
  - ユーザ名（任意。なくてもよい）
  - 公開鍵（1のコマンド実行後に秘密鍵のファイル名.pubで出力されるファイル）
  - 運用者か開発者か？
  ※所属グループとかシステム管理者にするかは管理側のグループでジャッジ？
3. 管理者は各サーバーの↑のシェルをSSH経由で実行する。
  - 1台ずつやると大変なので以下のようなシェルを作っておくとよいと考える。
  - パスワードは申請者から申請してもらってもよいが、以下の例では作成時に全サーバー同じになるようにしているので、ここで生成されたパスワードを申請者に連絡する想定です。（ほんとは発行されたパスワードをメールとチャットツール系などで伝達するのが理想だが。。。）

```shell-session
# パスワード生成
function _generate_password() {
  # cat /dev/urandom | tr -dc '!#$%&-@;:=~+0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' | head -c8; echo ""
  # 上だとすべての文字種に組み合わせにできないのでちょっと乱暴だが、文字種毎の利用数を固定にする
  L_CHARS=$(cat /dev/urandom | tr -dc '[:lower:]' | head -c3) # 英小文字3
  U_CHARS=$(cat /dev/urandom | tr -dc '[:upper:]' | head -c3) # 英大文字3
  N_CHARS=$(( RANDOM % 10 ))                                  # 数字1
  S_CHARS=$(cat /dev/urandom | tr -dc '!#$%&' | head -c1)     # 記号1
  # 結合した文字列をfoldで1文字ずつ分解（改行）して、並び順をシャッフルして最後に改行を除去する
  echo "${L_CHARS}${U_CHARS}${N_CHARS}${S_CHARS}" | fold -w1 | shuf | tr -d '\n'
  return 0
}
USER_PASSWORD=$(_generate_password)
echo ${USER_PASSWORD}
TARGET_HOSTS="guest1,guest2,guest3"
for TARGET in "guest1,guest2,guest3"; do
   ssh ${TARGET} "echo \"${USER_PASSWORD}\" | useradd.sh -a -c \"ユーザ名\" -g \"maintainers\"  \"ユーザID\" \"$(cat 申請された公開鍵のパス)\""
done
```

※↑のTARGET_HOSTSに記載した文字列は、`~/.ssh/cofig`に定義した接続定義名を想定。この接続ユーザは接続先ホストにおいてsudoでuseradd/usermod/usedelが実行可能なユーザである。

```~/.ssh/config
Host guest1
  HostName ホスト名 or IPアドレス
  User sudoでuseradd/usermod/usedelが実行可能なユーザ
  Port 22
  UserKnownHostsFile ~/.ssh/known_hosts
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile 秘密鍵のパス
  IdentitiesOnly yes

Host guest2
  HostName ホスト名 or IPアドレス
  User sudoでuseradd/usermod/usedelが実行可能なユーザ
  Port 22
  UserKnownHostsFile ~/.ssh/known_hosts
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile 秘密鍵のパス
  IdentitiesOnly yes

Host guest3
  HostName ホスト名 or IPアドレス
  User sudoでuseradd/usermod/usedelが実行可能なユーザ
  Port 22
  UserKnownHostsFile ~/.ssh/known_hosts
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile 秘密鍵のパス
  IdentitiesOnly yes
```

その他、作成した担当者が離任したら、管理者が削除シェルを実行して削除する。

# 余談

上のようなシェルを作ったが、台数増えるといろいろ面倒なのでユーザ管理部分はLDAPサーバーを立てた方がよさそう。。。