La foret rouge

root 계정 SSH 접속 제한 설정하는 Ansible Playbook 작성하기

Published on
Published on
Authors
  • avatar
    Name
    신주용

개요

SSH(Secure Shell)은 원격 서버에 안전하게 접속하하고 명령을 전송하는 데 사용 가능한 도구로 서버에 접속할 때 많이 사용됩니다1. 하지만 연결 방법이 안전하다고 해서 그 연결 자체가 안전하다는 것을 보장하지는 않습니다. 만약 권한이 없는 누군가가 안전하게(?) 우리 서버에 존재하는 특정 계정으로 접속할 수 있다면 그것은 굉장히 큰 문제로 이질 수 있습니다.

그리고 그 '특정 계정'이 될 수 있는 가장 쉬운 대상은 root 계정입니다. Linux 운영체제의 root 계정은 시스템 관리자 계정으로 운영체제의 모든 기능을 설정하고 변경할 수 있는 권한이 있습니다. 모든 시스템에 존재하고 막강한 권한이 있는 계정이다 보니 해커들은 이렇게 쉬운 공격 경로로 먼저 시도해보게 됩니다.

따라서 SSH 설정을 변경하여 root 계정의 원격 접속을 차단하는 것은 필수적인 보안 조치 중 하나입니다. 방법은 간단합니다. 설정 파일에서 PermitRootLogin no만 추가해주고 sshd를 재시작하면 됩니다. 그런데 이런 설정을 수십 대의 서버에 수동으로 해줘야 한다면 시간이 오래 소요되고 수동으로 하다 보면 사람인지라 실수를 할 수도 있습니다. 그래서 여러 대의 서버에서 효율적으로 수행할 수 있도록 Ansible Playbook을 활용하는 방법에 대해 설명하겠습니다.

단계

Ansible Playbook을 작성하기 전에 몇 가지 고려 사항이 있습니다.

  1. 만약 같은 설정 값이 여러 번 적용되어 있다면?
  • PermitRootLogin 설정은 Rocky Linux 9 버전을 기준으로는 기본적으로 파일 상단 쪽에 주석 처리 되어 있습니다. 누군가 이를 그대로 두고 파일 제일 아래에 PermitRootLogin yes를 설정해뒀는데 이번 취약점 조치를 위해 주석 처리 된 부분을 주석 해제 하여 PermitRootLogin이 두 번 존재하게 된다면 어떻게 될까요?
  • sshd_config 설정은 키-값 쌍으로 되어있는데, 같은 키가 여러 번 나오더라도 오류가 생기지 않고, 가장 먼저 나온 값이 사용됩니다2.
  1. 설정이 sshd_config 파일 외 다른 파일에 나뉘어 있다면?
  • 이 경우 일반적으로는 /etc/ssh/sshd_config.d 경로 아래에 *.conf 파일로 선언됩니다. 그리고 sshd_config 파일 상단을 보면 Include /etc/ssh/sshd_config.d/*.conf이 있습니다. 따라서 고려 사항 1번 내용에 따라 sshd_config.d 디렉터리 아래에서 설정된 값들이 더 우선 순위를 갖게 됩니다.
  1. root 말고 다른 계정이 없다면 접속을 어떻게 해야 하는가?
  • PermitRootLogin no를 설정하고 sshd 서비스를 재시작하면 root 계정으로 접속을 막을 수 있습니다. 다만, root 외 일반 사용자 계정이 하나도 없다면 해당 시스템으로 원격 접속이 불가합니다. (키보드 마우스 모니터 들고 직접 붙어야 합니다) 그러므로 재시작 전에 root가 아닌 일반 사용자 계정이 하나 이상 존재하는지 확인해야 합니다.

이런 점들을 고려하여 root 계정으로 SSH 접속을 제한하는 Ansible Playbook을 작성해봅시다.

1. 설정 파일 목록 모으기

SSH 설정 파일 목록을 모읍니다. 대상은 /etc/ssh/sshd_config 파일과 /etc/ssh/sshd_config.d/ 아래 *.conf로 끝나는 파일입니다. 모으기 쉽게 /etc/ssh 아래 모든 파일 중 sshd_config 키워드가 들어가는 단어만 필터링 했습니다.

- name: Target file list
  ansible.builtin.find:
    paths: "/etc/ssh"
    file_type: file
    recurse: true
  register: whole_file_list

- name: Filter files
  ansible.builtin.set_fact:
    file_list: >-
      {{
        whole_file_list.files
        | map(attribute='path')
        | select("search", "sshd_config")
        | reject("search", ".bak")
        | list
      }}
  • L#15: 나중 단계에 파일 백업 과정을 넣었는데 이 때 .bak을 붙이도록 했습니다. 따라서 이전 백업 파일을 다시 변경하지 않도록 필터링 합니다.

2. PermitRootLogin 키가 있는 파일만 필터링

이 키가 없는 파일은 사용하지 않습니다. grep 터미널 명령어를 활용해 파일 내용에 PermitRootLogin으로 시작하는 줄이 있는 (= 정상 실행 된 = return code가 0인) 파일들만 필터링합니다.

- name: Find files that `PermitRootLogin` option is enabled
  ansible.builtin.shell:
    cmd: grep -E "^(PermitRootLogin .+)$" "{{ item }}"
  loop: "{{ file_list }}"
  register: file_list
  failed_when: false
  changed_when: false

- name: Filter files
  ansible.builtin.set_fact:
    file_list: "{{ file_list.results | selectattr('rc', 'equalto', 0) | map(attribute='item') | list }}"

3. 파일 수정 전 백업

만약 문제가 있다면 복원 가능하도록 파일을 백업해둡니다. 동일 경로 원본 파일명 뒤 .bak.오늘날짜를 추가해서 복사합니다.

    - name: Backup `sshd_config` file
      ansible.builtin.copy:
        src: "/etc/ssh/sshd_config"
        dest: "/etc/ssh/sshd_config.bak.{{ ansible_date_time.date }}"
        remote_src: true

4. 설정 변경

드디어 sshd 설정을 변경합니다. 우선 고려사항 1번과 2번에 따라 PermitRootLogin 키가 여러 번 나올 수 있기 때문에 기존에 활성화 되어 있는 것은 다 주석으로 바꾸겠습니다. 그 다음 sshd_config 파일에만 PermitRootLogin no를 남기겠습니다.

    - name: Disable if enabled `PermitRootLogin` exist
      ansible.builtin.replace:
        path: "{{ item }}"
        regexp: "^(PermitRootLogin .+)$"
        replace: '# \1'
      loop: "{{ file_list }}"

    - name: Set `PermitRootLogin no`
      ansible.builtin.lineinfile:
        path: "/etc/ssh/sshd_config"
        regexp: '#\s?(PermitRootLogin .+)$'
        insertafter: "EOF"
        line: "PermitRootLogin no"
  • L#9-13: lineinfile은 파일에서 조건에 맞는 첫 번째로 매칭된 특정 문자열을 다른 문자열로 대치할 수 있습니다. 이전 task에서 주석 처리를 했으니 # 기호가 붙은 문장을 찾아 바꿉니다. sshd_config 파일에 저 설정이 없진 않겠지만 만일 매치되지 않는다면 파일의 제일 마지막에 PermitRootLogin no 문장을 추가합니다.

5. Non-root 유저 존재 확인

고려사항 3번에 따라 root 접속을 제한했을 때 원격 서버에 접속 가능한 일반 사용자 계정이 있는지 확인합니다.

그런데 일반 사용자 계정은 또 어떻게 알 수 있을까요? /etc/passwd 파일에서는 사용자 계정명과 UID, GID 등을 볼 수 있습니다. 이 중 UID가 1000이상이면서 60000 이하인 것들이 일반 사용자 계정입니다3.

만약 그런 계정이 하나도 없다면 설정 파일을 변경만 한 채로 sshd 서비스 재시작은 하지 않도록 fail로 처리합니다.

- name: Count non-root user
  ansible.builtin.command:
    cmd: "awk -F: '$3 >= 1000 && $3 < 60000 {print $1}' /etc/passwd"
  register: count_nonroot_user
  changed_when: false

- name: Ensure non-root user exists
  ansible.builtin.fail:
    msg: "No non-root users exist on the system! `sshd` restart aborted to prevent lockout."
  when: count_nonroot_user.stdout_lines | length < 1

6. sshd 서비스 재시작

마지막으로 서비스를 재시작해주면 이 다음부터는 ssh root@target-host와 같은 접속은 거부당하여 취약점을 제거할 수 있습니다.

- name: Restart `sshd`
  ansible.builtin.service:
    name: sshd
    state: restarted

전체 파일은 GitHub에서 확인하실 수 있습니다.

참고: 이 글에서 언급되었으나 깊게 설명하지 않은 내용입니다.

  • SSH
  • Ansible automation
  • Shell return code
  • Linux UID, GID, Systemd

Footnotes

  1. Cloudflare. "SSH란? | 보안 셸(SSH) 프로토콜." Cloudflare. https://www.cloudflare.com/ko-kr/learning/access-management/what-is-ssh/ (accessed Dec. 11, 2024).

  2. OpenBSD. "SSHD_CONFIG(5)." OpenBSD manual page server. https://man.openbsd.org/sshd_config (accessed Dec. 12, 2024).

  3. Rocky Linux. "User Management." Rocky Linux Documentation. https://docs.rockylinux.org/books/admin_guide/06-users/?h=uid_min#etclogindefs-file (accessed Dec. 13, 2024).