Ansible Playbook สำหรับ User Management: สร้าง User, Set Permissions

การจัดการ User บนเซิร์ฟเวอร์หลายเครื่องพร้อมกันเป็นงานที่ใช้เวลามากหากทำด้วยตนเอง ไม่ว่าจะเป็นการสร้าง account สำหรับ developer ใหม่, กำหนด sudo permissions, ตั้งค่า SSH key หรือลบ account ที่ไม่ใช้แล้ว การทำผิดพลาดเพียงครั้งเดียวอาจเปิดช่องโหว่ด้าน security ได้

บทความนี้จะแสดงวิธีเขียน Playbook สำหรับจัดการ User และ Permissions อย่างเป็นระบบ ครอบคลุมตั้งแต่สร้าง User, กำหนด Group, ตั้งค่า SSH key, ไปจนถึงลบ account และ rotate password ทั้งหมดในไฟล์เดียว

สร้าง User และกำหนด Group

Ansible ใช้ module user สำหรับจัดการ account บนระบบ Linux รองรับ Ubuntu, CentOS และ distro อื่นทุกตัว

---
- name: User Management Playbook
  hosts: all
  become: true

  vars:
    developers:
      - name: alice
        groups: ["sudo", "docker"]
        shell: /bin/bash
      - name: bob
        groups: ["docker"]
        shell: /bin/bash
      - name: carol
        groups: ["www-data"]
        shell: /bin/bash

  tasks:
    - name: Create developer accounts
      ansible.builtin.user:
        name: "{{ item.name }}"
        groups: "{{ item.groups }}"
        shell: "{{ item.shell }}"
        create_home: yes
        state: present
      loop: "{{ developers }}"

    - name: Create shared group for project
      ansible.builtin.group:
        name: devteam
        state: present

    - name: Add all developers to shared group
      ansible.builtin.user:
        name: "{{ item.name }}"
        groups: devteam
        append: yes
      loop: "{{ developers }}"

การใช้ append: yes มีความสำคัญมาก — ถ้าไม่ใส่ Ansible จะ replace groups ทั้งหมดด้วย group ที่ระบุ ทำให้ user อาจเสีย permissions ที่มีอยู่เดิมโดยไม่ตั้งใจ

ตั้งค่า SSH Key สำหรับ Users

การ deploy SSH public key ไปยังเซิร์ฟเวอร์หลายเครื่องพร้อมกันเป็นหนึ่งในงานที่ทำบ่อยที่สุด โดยเฉพาะเมื่อ developer คนใหม่เข้าทีม

---
- name: Deploy SSH Keys
  hosts: all
  become: true

  vars:
    ssh_users:
      - username: alice
        pubkey: "ssh-ed25519 AAAA...alice_key... alice@laptop"
      - username: bob
        pubkey: "ssh-ed25519 AAAA...bob_key... bob@workstation"

  tasks:
    - name: Deploy SSH public keys
      ansible.posix.authorized_key:
        user: "{{ item.username }}"
        key: "{{ item.pubkey }}"
        state: present
        exclusive: no
      loop: "{{ ssh_users }}"

    - name: Disable password authentication (enforce key-only)
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^PasswordAuthentication'
        line: 'PasswordAuthentication no'
        state: present
      notify: Restart SSH

  handlers:
    - name: Restart SSH
      ansible.builtin.service:
        name: sshd
        state: restarted

การใช้ exclusive: no จะ append key ใหม่เข้าไปโดยไม่ลบ key เดิม ถ้าตั้งเป็น exclusive: yes จะแทนที่ key ทั้งหมดด้วย key ที่ระบุ ซึ่งอาจทำให้ตัดการเชื่อมต่อของ user อื่นได้

กำหนด sudo Permissions แบบ Fine-grained

การให้ sudo access ทั้งหมด (ALL) เป็นแนวทางที่ไม่ปลอดภัย ควรกำหนด permission เฉพาะ command ที่จำเป็นเท่านั้น

---
- name: Configure sudo permissions
  hosts: all
  become: true

  tasks:
    - name: Grant full sudo to sysadmin group
      ansible.builtin.copy:
        dest: /etc/sudoers.d/sysadmin
        content: |
          %sysadmin ALL=(ALL) NOPASSWD: ALL
        owner: root
        group: root
        mode: '0440'
        validate: visudo -cf %s

    - name: Grant limited sudo to developer group (restart services only)
      ansible.builtin.copy:
        dest: /etc/sudoers.d/developers
        content: |
          %developers ALL=(ALL) NOPASSWD: /bin/systemctl restart nginx, /bin/systemctl restart php-fpm
        owner: root
        group: root
        mode: '0440'
        validate: visudo -cf %s

    - name: Grant deploy user permission to run deploy script
      ansible.builtin.copy:
        dest: /etc/sudoers.d/deploy
        content: |
          deploy ALL=(ALL) NOPASSWD: /opt/scripts/deploy.sh
        owner: root
        group: root
        mode: '0440'
        validate: visudo -cf %s

การใช้ validate: visudo -cf %s จะตรวจสอบ syntax ก่อน deploy จริง ป้องกันการ break sudoers file ที่อาจทำให้ lock ตัวเองออกจากระบบ

ลบ User และ Revoke Access

เมื่อ developer ลาออกหรือเปลี่ยนทีม การลบ account และ revoke access ทุก server ต้องทำอย่างรวดเร็วและครบถ้วน

---
- name: Offboard user
  hosts: all
  become: true

  vars:
    offboard_user: "alice"

  tasks:
    - name: Remove SSH authorized keys
      ansible.posix.authorized_key:
        user: "{{ offboard_user }}"
        key: ""
        state: absent
        exclusive: yes

    - name: Disable account (lock password)
      ansible.builtin.user:
        name: "{{ offboard_user }}"
        password_lock: yes

    - name: Kill active sessions
      ansible.builtin.command:
        cmd: "pkill -u {{ offboard_user }}"
      ignore_errors: yes
      changed_when: false

    - name: Remove sudoers entry
      ansible.builtin.file:
        path: "/etc/sudoers.d/{{ offboard_user }}"
        state: absent

    - name: Archive home directory before deletion
      ansible.builtin.archive:
        path: "/home/{{ offboard_user }}"
        dest: "/backup/{{ offboard_user }}_{{ ansible_date_time.date }}.tar.gz"
      ignore_errors: yes

    - name: Delete user account
      ansible.builtin.user:
        name: "{{ offboard_user }}"
        state: absent
        remove: yes

Playbook นี้ทำ offboarding อย่างปลอดภัย: ลบ SSH key ก่อน, lock password, kill sessions ที่ active อยู่, ลบ sudoers entry, backup home directory, แล้วจึงลบ account ทั้งหมด

Rotate Password สำหรับ Service Accounts

Service accounts เช่น deploy user หรือ monitoring user ควร rotate password เป็นประจำตาม security policy

---
- name: Rotate service account passwords
  hosts: all
  become: true

  vars:
    service_users:
      - deploy
      - monitoring

  tasks:
    - name: Generate random password
      ansible.builtin.set_fact:
        new_password: "{{ lookup('password', '/dev/null length=20 chars=ascii_letters,digits') }}"

    - name: Update password for service accounts
      ansible.builtin.user:
        name: "{{ item }}"
        password: "{{ new_password | password_hash('sha512') }}"
        update_password: always
      loop: "{{ service_users }}"
      no_log: true

    - name: Store new password in vault file
      ansible.builtin.lineinfile:
        path: /etc/ansible-vault/passwords.txt
        regexp: "^{{ item }}:"
        line: "{{ item }}: {{ new_password }}"
        create: yes
        mode: '0600'
      loop: "{{ service_users }}"
      no_log: true

การใช้ no_log: true จะซ่อน password ไม่ให้แสดงใน Ansible output หรือ log files ซึ่งสำคัญมากเมื่อทำงานกับข้อมูล sensitive

User Management แบบ Idempotent ด้วย Variable Files

สำหรับทีมขนาดใหญ่ ควรเก็บรายชื่อ user ไว้ใน variable file แยกต่างหาก ทำให้ Ops team แก้ไข user list ได้โดยไม่ต้องแตะ Playbook

# vars/users.yml
users:
  - name: alice
    uid: 1001
    comment: "Alice Developer"
    groups: ["sudo", "docker"]
    ssh_key: "ssh-ed25519 AAAA... alice@company"
    state: present
  - name: bob
    uid: 1002
    comment: "Bob Developer"
    groups: ["docker"]
    ssh_key: "ssh-ed25519 AAAA... bob@company"
    state: present
  - name: olduser
    uid: 1003
    comment: "Former Employee"
    groups: []
    state: absent   # จะถูกลบออก
# playbook.yml
---
- name: Sync user list
  hosts: all
  become: true
  vars_files:
    - vars/users.yml

  tasks:
    - name: Manage users from variable file
      ansible.builtin.user:
        name: "{{ item.name }}"
        uid: "{{ item.uid }}"
        comment: "{{ item.comment }}"
        groups: "{{ item.groups }}"
        append: yes
        create_home: yes
        state: "{{ item.state }}"
      loop: "{{ users }}"

    - name: Deploy SSH keys for active users
      ansible.posix.authorized_key:
        user: "{{ item.name }}"
        key: "{{ item.ssh_key }}"
        state: present
      loop: "{{ users | selectattr('state', 'equalto', 'present') | list }}"
      when: item.ssh_key is defined

การใช้ Jinja2 filter selectattr('state', 'equalto', 'present') กรองเฉพาะ user ที่ active ก่อน deploy SSH key ป้องกันการ deploy key ให้ user ที่ถูกลบไปแล้ว

สรุป

การจัดการ User ด้วย Automation ทำให้มั่นใจได้ว่าทุกเซิร์ฟเวอร์มี account, permissions, และ SSH key ที่ถูกต้องสม่ำเสมอ และเมื่อต้องลบ access สามารถทำได้ครบทุก server ในครั้งเดียวโดยไม่ตกหล่น

จุดสำคัญที่ต้องระวัง: ใส่ append: yes เสมอเมื่อเพิ่ม group, ใช้ validate: visudo -cf %s ก่อน deploy sudoers, และใช้ no_log: true ทุกครั้งที่ task เกี่ยวข้องกับ password