Почти каждая инфраструктура рано или поздно проходит через один и тот же этап взросления.

Сначала:

  • серверов мало;
  • все делается руками;
  • root-пароли живут в заметках;
  • SSH-доступ «вроде работает».

Потом серверов становится:

  • 20,
  • 50,
  • 200,
  • и внезапно оказывается, что инфраструктура уже напоминает археологический раскоп.

На одном сервере:

  • deploy

На другом:

  • ansible

На третьем:

  • вообще доступ только под root.

А где-то еще живет легендарный:

user: test
password: test123

Именно в этот момент обычно приходит осознание:
пора приводить SSH-доступ и пользователей к единому виду.

Что мы хотим получить

Нормальная современная схема выглядит так:

  • единый технический пользователь;
  • SSH-ключи вместо паролей;
  • sudo;
  • единая схема доступа;
  • возможность нормально использовать Ansible;
  • централизованное управление.

Подход №1 - Shell-скрипт

Это классический bootstrap-подход.

Когда:

  • Ansible еще нет;
  • доступ только по root;
  • нужно быстро подготовить сервер.

Shell все еще остается отличным инструментом для:

  • первоначальной инициализации;
  • rescue-сценариев;
  • cloud-init;
  • автоматической подготовки образов;
  • bootstrap новых серверов.

Но важно понимать:
shell - это скорее стартовая точка.

Современный shell-скрипт подготовки сервера

Вот вариант скрипта, который:

  • создает пользователя;
  • генерирует SSH-ключ;
  • настраивает authorized_keys;
  • выдает sudo;
  • отключает SSH-пароли;
  • подготавливает сервер к работе с Ansible.
#!/usr/bin/env bash

set -euo pipefail

############################################
# Настройки
############################################

USERNAME="devops"
USER_SHELL="/bin/bash"
SSH_KEY_TYPE="ed25519"
SSH_KEY_COMMENT="Generated by bootstrap script"
DISABLE_PASSWORD_AUTH="true"
DISABLE_ROOT_LOGIN="false"

# Дополнительные публичные ключи
AUTHORIZED_KEYS=(
"ssh-ed25519 AAAAC3NzaC1..."
"ssh-rsa AAAAB3Nza..."
)

############################################
# Проверка root
############################################

if [[ "$(id -u)" -ne 0 ]]; then
    echo "[ERROR] Скрипт нужно запускать от root"
    exit 1
fi

############################################
# Проверка пользователя
############################################

if id "$USERNAME" >/dev/null 2>&1; then
    echo "[INFO] Пользователь уже существует"
else
    echo "[INFO] Создаем пользователя"
    useradd \
        --create-home \
        --shell "$USER_SHELL" \
        "$USERNAME"
fi

############################################
# Добавление sudo
############################################

echo "[INFO] Настраиваем sudo"
cat <<EOF >/etc/sudoers.d/$USERNAME
$USERNAME ALL=(ALL) NOPASSWD: ALL
EOF

chmod 440 /etc/sudoers.d/$USERNAME

############################################
# Создание .ssh
############################################

SSH_DIR="/home/$USERNAME/.ssh"
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
chown -R "$USERNAME:$USERNAME" "$SSH_DIR"

############################################
# Генерация SSH ключа
############################################

KEY_PATH="$SSH_DIR/id_${SSH_KEY_TYPE}"
if [[ ! -f "$KEY_PATH" ]]; then
    echo "[INFO] Генерируем SSH ключ"
    sudo -u "$USERNAME" ssh-keygen \
        -t "$SSH_KEY_TYPE" \
        -N "" \
        -C "$SSH_KEY_COMMENT" \
        -f "$KEY_PATH"
else
    echo "[INFO] SSH ключ уже существует"
fi

############################################
# authorized_keys
############################################

AUTHORIZED_KEYS_FILE="$SSH_DIR/authorized_keys"
touch "$AUTHORIZED_KEYS_FILE"
chmod 600 "$AUTHORIZED_KEYS_FILE"

for KEY in "${AUTHORIZED_KEYS[@]}"; do
    if ! grep -qxF "$KEY" "$AUTHORIZED_KEYS_FILE"; then
        echo "$KEY" >> "$AUTHORIZED_KEYS_FILE"
    fi
done

############################################
# Добавляем локально сгенерированный ключ
############################################

GENERATED_PUBLIC_KEY=$(cat "${KEY_PATH}.pub")

if ! grep -qxF "$GENERATED_PUBLIC_KEY" "$AUTHORIZED_KEYS_FILE"; then
    echo "$GENERATED_PUBLIC_KEY" >> "$AUTHORIZED_KEYS_FILE"
fi

############################################
# Исправление владельца
############################################

chown -R "$USERNAME:$USERNAME" "$SSH_DIR"

############################################
# SSH hardening
############################################

if [[ "$DISABLE_PASSWORD_AUTH" == "true" ]]; then
    echo "[INFO] Отключаем PasswordAuthentication"
    sed -i \
        's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' \
        /etc/ssh/sshd_config
fi

if [[ "$DISABLE_ROOT_LOGIN" == "true" ]]; then
    echo "[INFO] Отключаем root login"
    sed -i \
        's/^#\?PermitRootLogin.*/PermitRootLogin no/' \
        /etc/ssh/sshd_config
fi

############################################
# Перезапуск SSH
############################################

systemctl restart ssh || systemctl restart sshd

############################################
# Информация
############################################

SERVER_IP=$(hostname -I | awk '{print $1}')

echo ""
echo "======================================"
echo "Пользователь успешно подготовлен"
echo "======================================"
echo "Сервер: $SERVER_IP"
echo "Пользователь: $USERNAME"
echo "SSH ключ:"
echo "${KEY_PATH}.pub"
echo "======================================"

В скрипте добавил:

set -euo pipefail

Это:

  • аварийное завершение при ошибках;
  • защита от неинициализированных переменных;
  • более безопасное выполнение.

Старые shell-скрипты без этого обычно работают по принципу:

«Ну ошибка и ошибка, едем дальше».

А потом половина сервера настроена, половина нет.

Добавил идемпотентность

Скрипт:

  • не пересоздает пользователя;
  • не дублирует ключи;
  • не ломает существующие настройки.

Используем

ed25519

Сейчас это уже стандарт де-факто:

ssh-keygen -t ed25519

Добавил SSH hardening

Автоматически:

  • отключаем парольную авторизацию;
  • можем отключить root login.

Но почему shell все равно проигрывает Ansible

Вот тут начинается самое интересное.

Проблемы shell-подхода

1. Плохо масштабируется

На 3 серверах:

  • нормально.

На 300:

  • начинается боль.

2. Нет нормального состояния системы

Shell не знает:

  • что уже применено;
  • что изменилось;
  • что сломалось.

Ansible это знает.

3. Сложно поддерживать

Через полгода shell-скрипт обычно выглядит как:

if grep ...

then

   sed ...

else

   awk ...

fi

А рядом комментарий:

# НЕ ТРОГАТЬ
# РАБОТАЕТ

Подход №2 - Ansible role

Вот тут уже начинается взрослая инфраструктура.

Ansible:

  • знает состояние;
  • умеет идемпотентность;
  • централизует управление;
  • нормально логирует изменения;
  • хорошо масштабируется.

Структура роли

roles/
└── prepare_access/
    ├── defaults/
    │   └── main.yml
    ├── tasks/
    │   └── main.yml
    ├── handlers/
    │   └── main.yml

defaults/main.yml

---
username: devops
user_shell: /bin/bash
generate_ssh_key: true
ssh_key_type: ed25519
ssh_key_comment: "Ansible generated key"
sudo_nopasswd: true
disable_ssh_password_auth: true
disable_root_login: false
additional_authorized_keys: []

tasks/main.yml

---
- name: Создание пользователя
  user:
    name: "{{ username }}"
    shell: "{{ user_shell }}"
    create_home: true
    state: present

- name: Добавление sudo
  user:
    name: "{{ username }}"
    groups: sudo
    append: true

- name: Создание .ssh
  file:
    path: "/home/{{ username }}/.ssh"
    state: directory
    owner: "{{ username }}"
    group: "{{ username }}"
    mode: '0700'

- name: Генерация SSH ключа
  openssh_keypair:
    path: "/home/{{ username }}/.ssh/id_{{ ssh_key_type }}"
    type: "{{ ssh_key_type }}"
    owner: "{{ username }}"
    group: "{{ username }}"
    mode: '0600'
    comment: "{{ ssh_key_comment }}"

- name: Чтение публичного ключа
  slurp:
    src: "/home/{{ username }}/.ssh/id_{{ ssh_key_type }}.pub"
  register: generated_pubkey

- name: Добавление generated key
  authorized_key:
    user: "{{ username }}"
    key: "{{ generated_pubkey['content'] | b64decode }}"

- name: Добавление дополнительных ключей
  authorized_key:
    user: "{{ username }}"
    key: "{{ item }}"
  loop: "{{ additional_authorized_keys }}"

- name: Настройка sudoers
  copy:
    dest: "/etc/sudoers.d/{{ username }}"
    content: |
      {{ username }} ALL=(ALL) NOPASSWD: ALL
    mode: '0440'

- name: Отключение SSH паролей
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^#?PasswordAuthentication'
    line: 'PasswordAuthentication no'
  notify: restart ssh

- name: Отключение root login
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^#?PermitRootLogin'
    line: 'PermitRootLogin no'
  when: disable_root_login
  notify: restart ssh

handlers/main.yml

---
- name: restart ssh
  service:
    name: ssh
    state: restarted

Что в итоге лучше

Shell подходит для:

  • bootstrap;
  • rescue;
  • cloud-init;
  • первоначальной инициализации.

Ansible подходит для:

  • production;
  • сопровождения;
  • поддержки;
  • масштабирования;
  • централизованного управления.

Итог

Shell-скрипты - это хороший старт.

Но Ansible - это уже инфраструктурная зрелость.

Когда:

  • все серверы одинаковы;
  • SSH стандартизирован;
  • доступ централизован;
  • изменения воспроизводимы -

инфраструктура перестает быть набором «магических серверов» и начинает становиться системой.

А это уже огромная разница между:

"работает - не трогай"

и:

"мы контролируем инфраструктуру"