From 571ddcc717feabd512bc64ac2bc7a0b18d45a2c1 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Mon, 7 Aug 2023 07:29:24 +0900 Subject: [PATCH] AWS user account management scripts updates - start option for create users (-g) - delete user script - update documentation - user lock user script in check user flow output - create user has check for valid username/group name --- Readme.md | 45 ++++--- backup/.gitignore | 2 + bin/authorized_key_location_change.sh | 2 +- bin/check_last_login.sh | 21 ++- bin/create_user.sh | 41 +++++- bin/delete_user.sh | 185 ++++++++++++++++++++++++++ bin/lock_user.sh | 12 +- bin/rename_user.sh | 43 ++++++ bin/unlock_user.sh | 12 +- user_create_flow.md | 7 +- 10 files changed, 323 insertions(+), 47 deletions(-) create mode 100644 backup/.gitignore create mode 100755 bin/delete_user.sh create mode 100644 bin/rename_user.sh diff --git a/Readme.md b/Readme.md index 51be455..bf2fd09 100644 --- a/Readme.md +++ b/Readme.md @@ -11,22 +11,27 @@ The folder holding the script must be owned by *root* and have *600* permissions ```sh cd /root/ -git clone https://git.tequila.jp/ScriptsCollections/AwsUserCreate.git users +git clone http://gitlab-ap.factory.tools/scripts-collections/aws-user-create.git users chown root. users chgrp 600 users ``` -Alternate download: `git clone http://gitlab-ap.factory.tools/scripts-collections/aws-user-create.git users` +Alternate download: +`git clone https://git.tequila.jp/ScriptsCollections/AwsUserCreate.git users` ## Folders Inside the base folder there are -* ssh-keygen for temporary holding the PEM/PUB files -* zip file which holds the created user list, password and PEM/PUB files +- ssh-keygen for temporary holding the PEM/PUB files +- zip file which holds the created user list, password and PEM/PUB files ## Options +### -g (go) + +If not set, the script will not run. + ### -t (test) Run in test mode. This will *NOT* create any groups or users. Nor will it create any ssh key files. @@ -42,14 +47,15 @@ In the `/root/users/` folder there needs to be a file called '*user_list.txt*' This is a CSV type file with the following layout -ID | Username | Group | Optional Password | Override host name | Override ssh key type --|-|-|-|-|- +ID | Username | Group and Sub Group | SSH Access Type | Optional Password | Override host name | Override ssh key type +-|-|-|-|-|-|- -The ID, Username and Group column must be filled. -For sub groups add them with a *,* The first group is the master group -If the password column is filled, the string from here will be used as the PEM Key password. -If a override hostname is set it will be used instead of `hostname` -If the ssh key type is set, it will override the default *ed25519* type. This is not recommended. Only *rsa* is allowed. This is for setting up backwards compatible lists. +1: The ID, Username and Group column must be filled. +2: For sub groups add them with a *,* The first group is the master group +3: SSH Access type as: allow/forward. allow is default +4: If the password column is filled, the string from here will be used as the PEM Key password. +5: If a override hostname is set it will be used instead of `hostname` +6: If the ssh key type is set, it will override the default *ed25519* type. Only *rsa* is allowed. This is for setting up backwards compatible lists. Change is not recommended The ID can be any string in any form. It can also be left empty. It is not used at the moment @@ -59,10 +65,11 @@ The file can hold comments. The first character in the line must be a *#* Example file ```csv -user1;some.name;group-a;;hostname -user2;othername;group-a;; +#user_id;user_name;group,subgroup;ssh access type;override password;override hostname;override ssh type +user1;some.name;group-a;allow;;hostname; +user2;othername;group-a;allow;;; # I am a comment -;username;groupC;setpassword; +;username;groupC;allow;setpassword;; ... ``` @@ -131,7 +138,7 @@ If the public pem file is already provided the output will be a bit different ```txt ++ Create 'some.name:group-a' - < Use existing public ssh key '/root/users/ssh-keygen/hostname#group-a#some.name#ed25519.pem.pub' + < Use existing public ssh key '/root/users/ssh-keygen-created-pub/hostname#group-a#some.name#ed25519.pem.pub' > Create .ssh folder > Add public into authorized_keys > Secure folder .ssh and authorized_keys file @@ -187,9 +194,9 @@ The SSH PEM key password can be reset or changed with To remove the password use this `-N ""` -**NOTE** -If the command is used like this it will be stored in the history file. -For scurity reason it is recommended to not give the -P and -N options when changing the password. +> [!notice] +> If the command is used like this it will be stored in the history file. +> For scurity reason it is recommended to not give the -P and -N options when changing the password ### Missing PUB key @@ -197,7 +204,7 @@ The public key part can be extracted from the SSH PEM key with `$> ssh-keygen -y -f [PEM].pem > [PEM].pem.pub` -*[PEM]* is the placeholder for the filename +`[PEM]` is the placeholder for the filename ## Lock and unlock uses diff --git a/backup/.gitignore b/backup/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backup/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/bin/authorized_key_location_change.sh b/bin/authorized_key_location_change.sh index 6211fd1..5a6e17c 100755 --- a/bin/authorized_key_location_change.sh +++ b/bin/authorized_key_location_change.sh @@ -8,7 +8,7 @@ SKIP_USERS=(); while getopts ":gls:" opt; do case "${opt}" in g|go) - # default we + # default we test TEST=0; ;; s|skip) diff --git a/bin/check_last_login.sh b/bin/check_last_login.sh index ade5e32..14ab7ab 100755 --- a/bin/check_last_login.sh +++ b/bin/check_last_login.sh @@ -16,8 +16,7 @@ max_age_create=30; # one day in seconds day=86400; # delete account strings -delete_accounts=""; -user_group_tpl="gpasswd -d %s %s;gpasswd -a %s %s;"; +lock_accounts=""; # log base folder LOG="${BASE_FOLDER}/../log"; # auth log file user;date from collect_login_data script @@ -48,7 +47,7 @@ echo "Checking Group : ${ssh_group}"; continue; fi; account_age=0; - delete_user=0; + lock_user=0; out_string=""; #echo "* Checking user ${username}"; # check user create time, if we have set it in comment @@ -80,7 +79,7 @@ echo "Checking Group : ${ssh_group}"; last_login=$(awk '{printf("%.0f\n",($1-$2)/$3)}' <<<"${now} ${last_login_date} ${day}"); if [ ${last_login} -gt ${max_age_login} ]; then out_string="[!] last ssh log in ${last_login} days ago"; - delete_user=1; + lock_user=1; else out_string="OK [ssh]"; fi; @@ -93,7 +92,7 @@ echo "Checking Group : ${ssh_group}"; last_login=$(awk '{printf("%.0f\n",($1-$2)/$3)}' <<<"${now} ${last_login_date} ${day}"); if [ ${last_login} -gt ${max_age_login} ]; then out_string="[!] last terminal log in ${last_login} days ago"; - delete_user=1; + lock_user=1; else out_string="OK [lastlog]"; fi; @@ -105,7 +104,7 @@ echo "Checking Group : ${ssh_group}"; account_age=$(awk '{printf("%.0f\n",($1-$2)/$3)}' <<<"${now} ${user_create_date} ${day}"); if [ ${account_age} -gt ${max_age_create} ]; then out_string="[!] Never logged in, account created ${account_age} days ago"; - delete_user=1; + lock_user=1; else out_string="OK [first login]"; fi; @@ -113,8 +112,8 @@ echo "Checking Group : ${ssh_group}"; out_string="[!!!] Never logged in and we have no create date"; fi; # build delete output - if [ ${delete_user} = 1 ]; then - delete_accounts="${delete_accounts}"$(printf "${user_group_tpl}" "${username}" "${ssh_group}" "${username}" "${ssh_reject_group}")$'\n'; + if [ ${lock_user} = 1 ]; then + lock_accounts="${lock_accounts} ${username}" fi; printf "* Checking user %-20s: %s\n" "${username}" "${out_string}"; done; @@ -124,11 +123,11 @@ echo "Showing current SSH Reject users:" for user in $(cat /etc/group|grep "${ssh_reject_group}:" | cut -d ":" -f 4 | sed -e 's/,/ /g'); do echo "${user}"; done; -if [ ! -z "${delete_accounts}" ]; then +if [ ! -z "${lock_accounts}" ]; then echo "--------------------->" - echo "% Run list below to move users to reject ssh group"; + echo "% Run script below to move users to reject ssh group"; echo ""; - echo "${delete_accounts}"; + echo "bin/lock_user.sh ${lock_accounts}"; fi; echo "[END] ===============>" diff --git a/bin/create_user.sh b/bin/create_user.sh index c12b940..46acf93 100755 --- a/bin/create_user.sh +++ b/bin/create_user.sh @@ -25,10 +25,13 @@ # They pem pub key must follow the set rules above # SET TO 1 to TEST [will not create user/group/folder] -TEST=0; # no creation except ssh keys +TEST=0; # no actions will be run INFO=0; # no creation of anything, just print info strings +GO=1; # without this flag the script will exit with an info box while getopts ":tih:" opt; do case "${opt}" in + g|go) + GO=1; t|test) TEST=1; ;; @@ -41,6 +44,7 @@ while getopts ":tih:" opt; do \?) echo -e "\n Option does not exist: ${OPTARG}\n"; echo "Use -t for test and -i for info"; + echo "Use -g for actually creation run"; echo "Override default /home/ folder location with -h "; exit 1; ;; @@ -153,6 +157,14 @@ if [ $(whoami) != "root" ]; then echo "!!!! Script must be run as root user !!!!"; fi; fi; + +# exit if not -g parameter set +if [ $GO -eq 0 ]; then + echo "Script has to be run with -g option for actual user creation."; + echo "It is recommended to run -t for testing prior to user creation."; + exit; +fi; + # create users cat "${ROOT_FOLDER}${input_file}" | while read i; do @@ -162,6 +174,13 @@ while read i; do fi; # POS 2: make lower case, remove spaces username=$(echo "${i}" | cut -d ";" -f 2 | tr A-Z a-z | tr -d ' '); + # check username is alphanumeric with . + if ! [[ "${username}" =~ ^[a-z0-9]+([.a-z0-9_-]+[a-z0-9])?$ ]]; then + echo "User name can only be a-z 0-9 - _ . and cannot start or end with - . or _: ${username}"; + if [ ${TEST} -eq 0 ]; then + break; + fi; + fi; # POS 3: groups _group=$(echo "${i}" | cut -d ";" -f 3 | tr A-Z a-z | tr -d ' '); group=$(echo "${_group}" | cut -d "," -f 1); @@ -175,7 +194,9 @@ while read i; do fi; if [ $ssh_forward_ok -eq 0 ] && [ "${ssh_access_type}" = "forward" ]; then echo "[!!!] sshforward group does not exsts, cannot set user ${username}"; - break; + if [ ${TEST} -eq 0 ]; then + break; + fi; fi; ssh_group="ssh${ssh_access_type}"; # sshallow group is always added @@ -206,7 +227,21 @@ while read i; do if [ -z "${username}" ] || [ -z "${_group}" ]; then echo "[!!!!!] Missing user or group entry for ${username}/${_group}"; echo "[*** ABORT RUN ***]" - break; + if [ ${TEST} -eq 0 ]; then + break; + fi; + else + group_error=0; + # check group names valid + for create_group in ${_group//,/ }; do + if ! [[ "${create_group}" =~ ^[a-z0-9]+([a-z0-9_-]+[a-z0-9])?$ ]]; then + echo "Group name can only be a-z 0-9 - _ and cannot start or end with - or _: ${create_group}"; + group_error=1; + fi; + done; + if [ $group_error -eq 1 ] && [ ${TEST} -eq 0 ]; then + break; + fi; fi; # SSH file name part without folder ssh_keygen_id="${hostname}${separator}${group}${separator}${username}${separator}${ssh_keytype}.pem"; diff --git a/bin/delete_user.sh b/bin/delete_user.sh new file mode 100755 index 0000000..be9d9e0 --- /dev/null +++ b/bin/delete_user.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash + +# Delete user +# - Backup +# - delete user +# - delete home +# - remove ssh-keygen-created-pub files +# - remove ssh central auth data if exits +# - update user_list.txt and comment (#) line for this user +# - write delete log + +# This will permaently remove the user + +TEST=0; # do not run any actions +GO=1; # without this flag the script will exit with an info box +BACKUP=1; +while getopts ":tih:" opt; do + case "${opt}" in + g|go) + GO=1; + ;; + t|test) + TEST=1; + ;; + b|nobackup) + BACKUP=0; + ;; + \?) + echo -e "\n Option does not exist: ${OPTARG}\n"; + echo "Use -t for test"; + echo "Use -g for actually creation run"; + echo "Use -b to not make a backup of the home folder and public key" + exit 1; + ;; + esac; +done; +shift "$((OPTIND-1))" + +if [ $(whoami) != "root" ]; then + if [ ${TEST} -eq 0 ]; then + echo "Script must be run as root user"; + exit; + else + echo "!!!! Script must be run as root user !!!!"; + fi; +fi; + +if [ $# -eq 0 ]; then + echo "Must give at least one user name"; + exit; +fi; + +# check tar, bzip2 is installed if backup = 1 + +host=$(hostname); +timestamp=$(date +%Y%m%d-%H%M%S) +# character to set getween info blocks +separator="#"; +# base folder for all data +BASE_FOLDER=$(dirname $(readlink -f $0))"/"; +root_folder="${BASE_FOLDER}../"; +backup_folder="${BASE_FOLDER}../backup/"; +SSH_KEYGEN_FOLDER_CREATED_PUB='ssh-keygen-created-pub/'; +input_file='user_list.txt'; +user_list_file="${root_folder}${input_file}"; +# log file +LOG="${BASE_FOLDER}/../log/delete_user."$(date +"%F_%H%m%S"); +if [ ${TEST} -eq 0 ]; then + LOG="${LOG}.log"; +else + LOG="${LOG}.test.log"; +fi; +# ignore users (root and admin users) +ignore_users=('root' 'ec2-user' 'ubuntu' 'admin'); +# detect ssh authorized_keys setting +SSH_CENTRAL_AUTHORIZED_FILE_FOLDER=''; +SSH_AUTHORIZED_FILE=''; +for cf in $(grep "^AuthorizedKeysFile" /etc/ssh/sshd_config | grep "%u"); do + if [ ! -z $(echo "${cf}" | grep "%u") ]; then + SSH_CENTRAL_AUTHORIZED_FILE_FOLDER=$(echo "${cf}" | sed -e 's/%u//'); + if [ ! -d "${SSH_CENTRAL_AUTHORIZED_FILE_FOLDER}" ]; then + echo "ssh central authorized_file folder could not be found: ${SSH_CENTRAL_AUTHORIZED_FILE_FOLDER}"; + exit; + fi; + fi; +done; + +if [ ! -f "${user_list_file}" ]; then + echo "${input_file} is missing"; + exit; +fi; + +# $1 ... $n +for username in "$@"; do + # skip if there is an option hidden + if [[ ${_arg:0:1} = "-" ]]; then + continue; + fi; + # skip ignore users, note that if a user is not in the sshallow list anyway + # we skip them too, this is just in case check + if [[ " ${ignore_users[*]} " =~ " ${username} " ]]; then + echo "[!] User ${username} is in the ignore user list"; + continue; + fi; + + # user must exist in user_list.txt and /etc/passwd + # if missing in or another do not continue + if ! id "${username}" &>/dev/null; then + # not in passwd + echo "[!!!] User ${username} does not exist in /etc/passwd"; + if [ ${TEST} -eq 0 ]; then + break; + fi; + fi; + user_list_entry=$(grep "${username}" "${user_list_file}"); + if [ -z "${user_list_entry}" ]; then + echo "[!!!] User ${username} does not exist in user_list.txt file"; + if [ ${TEST} -eq 0 ]; then + break; + fi; + elif [[ "${user_list_entry}" =~ ^#DELETED ]]; then + echo "[!!!] User ${username} is flagged as deleted in user_list.txt file"; + if [ ${TEST} -eq 0 ]; then + break; + fi; + fi; + + echo "=> Delete: ${username}"; + # ssh authorized file + SSH_AUTHORIZED_FILE="${SSH_CENTRAL_AUTHORIZED_FILE_FOLDER}${username}"; + + # make backup from /home + if [ ${BACKUP} -eq 1 ]; then + home_folder=$(getent passwd ${username} | cut -d ":" -f 6); + backup_file="${backup_folder}${host}${separator}${username}.${timestamp}.tar.bz2"; + files_list="${home_folder}"; + if [ -f "${SSH_AUTHORIZED_FILE}" ]; then + files_list="${files_list} ${SSH_AUTHORIZED_FILE}"; + fi; + echo "[0] Backup ${files_list} to ${backup_file}"; + if [ ${TEST} -eq 0 ]; then + tar cfjp "${backup_file}" ${file_list}; + else + echo "$> tar cfjp \"${backup_file}\" ${files_list};"; + fi; + fi; + + echo "[1] Remove user + home dir"; + if [ ${TEST} -eq 0 ]; then + userdel -r ${username} + else + echo "$> userdel -r ${username}"; + fi; + + # remove ssh files in pub + echo "[2] Remove SSH Public key"; + # Note, we keep the public key in the -created-pub folder + if [ -f "${SSH_AUTHORIZED_FILE}" ]; then + if [ ${TEST} -eq 0 ]; then + chattr -i "${SSH_AUTHORIZED_FILE}"; + rm "${SSH_AUTHORIZED_FILE}"; + else + echo "$> chattr -i \"${SSH_AUTHORIZED_FILE}\";"; + echo "$> rm \"${SSH_AUTHORIZED_FILE}\";" + fi; + else + # Not critical error + echo "[?] Cannot find ${SSH_AUTHORIZED_FILE}"; + fi; + + # Update user_list.txt file and add # for the line + echo "[3] Update user_list.txt file"; + # eg n;foo -> #DELETED-YYYYMMDD_HHmmss:n;foo ... + delete_date=$(date +%Y%m%d_%H%M%S) + if [ ${TEST} -eq 0 ]; then + sed -i -e "s/^\([A-Za-z0-9]\{1,\};${username};\)/#DELETED-${delete_date}:\1/" "${user_list_file}"; + else + echo "$> sed -i -e \"s/^\([A-Za-z0-9]\{1,\};${username};\)/#DELETED-${delete_date}:\1/\" \"${user_list_file}\";"; + fi; + + echo $(date +"%F %T")";${host};${username}" >> "${LOG}"; + +done; + +# __END__ diff --git a/bin/lock_user.sh b/bin/lock_user.sh index f8174b7..dff7cf0 100755 --- a/bin/lock_user.sh +++ b/bin/lock_user.sh @@ -52,17 +52,17 @@ for username in "$@"; do # skip ignore users, note that if a user is not in the sshallow list anyway # we skip them too, this is just in case check if [[ " ${ignore_users[*]} " =~ " ${username} " ]]; then - echo "[!] User $username is in the ignore user list"; + echo "[!] User ${username} is in the ignore user list"; continue; fi; # check that user exists in passwd if ! id "${username}" &>/dev/null; then - echo "[!] User $username does not exists in /etc/passwd file"; + echo "[!] User ${username} does not exists in /etc/passwd file"; continue; fi; # if not check if in reject list if id -nGz "${username}" | grep -qzxF "${ssh_reject_group}"; then - echo "[.] User $username already in the ${ssh_reject_group} list"; + echo "[.] User ${username} already in the ${ssh_reject_group} list"; continue; fi; # check if user is in sshallow/forward list @@ -73,14 +73,14 @@ for username in "$@"; do # if user is in ssh allow group and ALSO in ssh forward group -> bad if id -nGz "${username}" | grep -qzxF "${ssh_forward_group}"; then if [ ! -z "${ssh_remove_group}" ]; then - echo "[!!!! ERROR !!!!] User $username exists in both ${ssh_allow_group} and ${ssh_forward_group} group which should not be allowed. Remove user from one group and run script again."; + echo "[!!!! ERROR !!!!] User ${username} exists in both ${ssh_allow_group} and ${ssh_forward_group} group which should not be allowed. Remove user from one group and run script again."; break; fi; ssh_remove_group="${ssh_forward_group}"; fi; if [ ! -z "${ssh_remove_group}" ]; then # remove user from ssh group and add to reject groups - echo "[*] User $username will be removed from ${ssh_remove_group}"; + echo "[*] User ${username} will be removed from ${ssh_remove_group}"; if [ ${TEST} -eq 1 ]; then printf "${user_group_tpl}" "${username}" "${ssh_remove_group}" "${username}" "${ssh_reject_group}"; else @@ -89,7 +89,7 @@ for username in "$@"; do fi; else # skip not ssh user - echo "[?] User $username not in any ssh allow/foward groups"; + echo "[?] User ${username} not in any ssh allow/foward groups"; fi; done; diff --git a/bin/rename_user.sh b/bin/rename_user.sh new file mode 100644 index 0000000..ead971f --- /dev/null +++ b/bin/rename_user.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# Rename user +# - rename user name +# - rename home folder + owner +# - rename public key file in /etc/ssh/ + +TEST=0; # do not run any actions +GO=1; # without this flag the script will exit with an info box +BACKUP=1; +while getopts ":tih:" opt; do + case "${opt}" in + g|go) + GO=1; + ;; + t|test) + TEST=1; + ;; + \?) + echo -e "\n Option does not exist: ${OPTARG}\n"; + echo "Use -t for test"; + echo "Use -g for actually creation run"; + exit 1; + ;; + esac; +done; +shift "$((OPTIND-1))" + +if [ $(whoami) != "root" ]; then + if [ ${TEST} -eq 0 ]; then + echo "Script must be run as root user"; + exit; + else + echo "!!!! Script must be run as root user !!!!"; + fi; +fi; + +if [ $# -eq 0 ]; then + echo "Must give at least one user name"; + exit; +fi; + +# __END__ diff --git a/bin/unlock_user.sh b/bin/unlock_user.sh index 05f74dd..5a1082b 100755 --- a/bin/unlock_user.sh +++ b/bin/unlock_user.sh @@ -66,21 +66,21 @@ for username in "$@"; do # skip ignore users, note that if a user is not in the sshallow list anyway # we skip them too, this is just in case check if [[ " ${ignore_users[*]} " =~ " ${username} " ]]; then - echo "[!] User $username is in the ignore user list"; + echo "[!] User ${username} is in the ignore user list"; continue; fi; # check that user exists in passwd if ! id "${username}" &>/dev/null; then - echo "[!] User $username does not exists in /etc/passwd file"; + echo "[!] User ${username} does not exists in /etc/passwd file"; continue; fi; # check if already in OK groups if id -nGz "${username}" | grep -qzxF "${ssh_allow_group}"; then - echo "[.] User $username already in the ${ssh_allow_group} list"; + echo "[.] User ${username} already in the ${ssh_allow_group} list"; continue; fi; if id -nGz "${username}" | grep -qzxF "${ssh_forward_group}"; then - echo "[.] User $username already in the ${ssh_forward_group} list"; + echo "[.] User ${username} already in the ${ssh_forward_group} list"; continue; fi; # try to find user in user_list.txt and get the allow/forward flag from there, @@ -98,7 +98,7 @@ for username in "$@"; do # check if user is in reject group remove if id -nGz "${username}" | grep -qzxF "${ssh_reject_group}"; then # remove user from ssh group and add to reject groups - echo "[*] User $username will be added to ${ssh_add_group}"; + echo "[*] User ${username} will be added to ${ssh_add_group}"; if [ ${TEST} -eq 1 ]; then printf "${user_group_tpl}" "${username}" "${ssh_reject_group}" "${username}" "${ssh_add_group}"; else @@ -107,7 +107,7 @@ for username in "$@"; do fi; else # skip not ssh user - echo "[?] User $username not in the ssh reject group"; + echo "[?] User ${username} not in the ssh reject group"; fi; done; diff --git a/user_create_flow.md b/user_create_flow.md index 3c2eedc..bac9e3f 100644 --- a/user_create_flow.md +++ b/user_create_flow.md @@ -1,11 +1,16 @@ # AWS user create flow +**NOTE** The script will check in the /etc/ssh/sshd_config for `AuthorizedKeysFile` keyword with `/etc/ssh/authorized_keys/%u`. If this exists it will move the ssk keys from the users home folder the folder `/etc/ssh/authorized_keys` with the created username as file name + * Step 1: check if main group exists * Step 2: Add user to user_list.txt -Example: +Very basic example with minimum settings: + +SSH Type will default to EP25519 ```txt +#user_id;user_name;group,subgroup;ssh access type;override password;override hostname;override ssh type # 2022-12-12 1;test.foo;group-a;allow ```