User & Permission #
Dalam arsitektur infrastruktur server modern, manajemen user dan permission adalah pondasi utama keamanan sistem. Melakukan manajemen ini secara manual pada puluhan atau ratusan server tidak hanya menghabiskan waktu, tetapi juga sangat rentan terhadap kesalahan manusia, seperti pemberian hak sudo yang berlebihan, password yang lemah, atau SSH key yang tidak dibersihkan setelah admin keluar. Ansible menawarkan pendekatan terotomatisasi, deklaratif, dan idempoten untuk mengelola siklus hidup user, keanggotaan grup, konfigurasi shell, autentikasi SSH, dan eskalasi hak akses (privilege escalation) secara terpusat. Dengan Ansible, kita bisa memastikan bahwa seluruh sistem berada dalam keadaan aman, terdokumentasi, dan sesuai dengan standar kepatuhan keamanan yang berlaku.
Filosofi dan Tantangan Manajemen User di Server Produksi #
Ketika kita mengelola satu atau dua server, menginput perintah adduser atau mengedit file /etc/sudoers secara manual mungkin tampak mudah. Namun, seiring dengan skala infrastruktur kita yang berkembang menjadi puluhan atau ratusan node, beberapa tantangan kritis akan mulai muncul. Salah satu tantangan terbesar adalah inkonsistensi UID (User ID) dan GID (Group ID). Tanpa otomasi, sistem operasi di setiap server akan menetapkan UID/GID berikutnya yang tersedia secara acak berdasarkan urutan pembuatan user. Hal ini dapat menimbulkan masalah besar ketika kita menggunakan penyimpanan bersama seperti NFS atau GlusterFS, di mana hak akses file bergantung pada kesamaan UID dan GID di seluruh node.
Selain itu, manajemen kunci SSH secara manual sangat rentan terhadap celah keamanan. Ketika seorang developer atau system administrator keluar dari tim, menghapus kunci public SSH mereka dari setiap file authorized_keys di setiap server adalah tugas yang rentan terlewat. Akibatnya, server kita mungkin tetap terbuka bagi pihak yang sudah tidak memiliki wewenang. Begitu pula dengan pemberian hak akses administratif. Tanpa konfigurasi sudoers yang terpusat dan terabstraksi, kita sering kali melihat praktik buruk di mana user diberi hak akses penuh tanpa batasan, atau bahkan menggunakan user root secara langsung untuk operasional sehari-hari.
Ansible mengatasi masalah ini dengan memperlakukan konfigurasi user sebagai kode (User Management as Code). Kita mendefinisikan keadaan akhir (desired state) dari identitas server kita di dalam file YAML, dan Ansible bertugas memastikan keadaan tersebut terwujud pada setiap managed node secara konsisten dan idempoten.
flowchart TD
A["Mulai Playbook Ansible"] --> B["1. Pastikan Grup Sistem & Aplikasi Ada"]
B --> C["2. Buat User Sistem (nologin)"]
B --> D["3. Buat User Operator (deployer)"]
C --> E["Terapkan Konfigurasi UID/GID Konsisten"]
D --> F["Terapkan Password SHA-512 & Shell Bash"]
F --> G["4. Distribusikan SSH Public Keys"]
G --> H["5. Tulis Sudoers dengan Validasi visudo"]
H --> I["Selesai: Server Siap Digunakan"]
Mengelola Grup Sistem dan Grup Kustom #
Sebelum membuat user, kita harus memastikan bahwa grup yang akan menaungi user tersebut sudah ada di sistem. Grup di Linux berfungsi sebagai lapisan abstraksi pertama dalam pemberian hak akses file dan direktori. Ansible menyediakan module ansible.builtin.group untuk mengelola grup sistem.
Ada dua parameter penting dalam modul grup: name untuk menentukan nama grup, dan gid untuk menentukan ID grup secara eksplisit. Menentukan gid secara eksplisit sangat direkomendasikan untuk grup-grup aplikasi kustom agar kita mendapatkan konsistensi penuh di seluruh klaster server.
Mari kita perhatikan perbedaan antara penulisan anti-pattern dan solusi yang direkomendasikan berikut ini:
# ANTI-PATTERN: Membuat grup tanpa menentukan GID secara eksplisit, menyebabkan ID acak antar server
- name: Buat grup deployer
group:
name: deployer
state: present
# BENAR: Menentukan GID secara eksplisit untuk menjaga konsistensi UID/GID di seluruh infrastruktur kita
- name: Buat grup deployer dengan GID tetap
group:
name: deployer
gid: 2000
state: present
Ketika kita menentukan gid: 2000, Ansible akan memastikan grup deployer selalu menggunakan GID 2000 di server Ubuntu, CentOS, Debian, maupun RHEL. Jika grup sudah ada namun menggunakan GID yang berbeda, Ansible akan memperbaruinya (kecuali jika GID tersebut sudah digunakan oleh grup lain, di mana Ansible akan memicu error untuk mencegah konflik).
Kita juga dapat mengelola beberapa grup sekaligus menggunakan perulangan (loop) agar kode playbook kita menjadi lebih bersih dan modular:
- name: Pastikan grup-grup operasional terkonfigurasi
group:
name: "{{ item.name }}"
gid: "{{ item.gid }}"
state: "{{ item.state | default('present') }}"
loop:
- { name: 'adminers', gid: 2100 }
- { name: 'developers', gid: 2200 }
- { name: 'auditors', gid: 2300 }
- { name: 'oldgroup', gid: 2400, state: 'absent' }
loop_control:
label: "{{ item.name }}"
Dalam contoh di atas, kita tidak hanya membuat grup baru, tetapi juga memastikan grup lama yang sudah tidak digunakan (oldgroup) dihapus dengan menyetel state: absent.
Siklus Hidup Akun User dengan Module user #
Setelah grup siap, kita bisa mulai mengelola akun user menggunakan module ansible.builtin.user. Module ini mengabstraksikan perintah backend Linux seperti useradd, usermod, dan userdel menjadi parameter deklaratif.
Berikut adalah beberapa parameter kunci yang wajib kita pahami:
name: Username yang akan dikelola di server target.uid: User ID unik untuk user tersebut. Seperti halnya GID, menyetel UID secara manual sangat dianjurkan untuk user non-sistem demi konsistensi.group: Menetapkan grup utama (primary group) untuk user.groups: Menetapkan daftar grup tambahan (supplementary groups) dalam bentuk list.append: Ini adalah parameter yang sangat krusial. Jika disetel ketrue, Ansible akan menambahkan user ke grup baru tanpa menghapus user tersebut dari grup lama yang tidak disebutkan di parametergroups. Jika disetel kefalse(default), user akan dihapus dari seluruh grup tambahan yang tidak terdaftar di playbook kita.shell: Menentukan login shell untuk user. Untuk user interaktif, kita biasanya menggunakan/bin/bash. Namun, untuk service account (misalnya user yang hanya digunakan untuk menjalankan daemon database atau monitoring), kita wajib menyetelnya ke/usr/sbin/nologinatau/sbin/nologinagar akun tersebut tidak bisa digunakan untuk login secara langsung.create_home: Menentukan apakah Ansible harus membuat home directory (misalnya/home/username) atau tidak.system: Jika disetel ketrue, user akan dibuat sebagai system user dengan UID di bawah 1000 dan tanpa batas waktu kedaluwarsa password.
Mari kita lihat contoh konfigurasi lengkap siklus hidup user:
- name: Kelola user deployment dan service account
block:
# Membuat user interaktif dengan grup tambahan
- name: Buat user operator deployer
user:
name: deployer
uid: 2001
group: deployers
groups:
- sudo
- docker
append: true
shell: /bin/bash
create_home: true
state: present
# Membuat service account non-interaktif demi keamanan
- name: Buat service account untuk Prometheus
user:
name: prometheus
shell: /usr/sbin/nologin
system: true
create_home: false
state: present
# Menghapus user lama secara bersih
- name: Hapus user mantan karyawan
user:
name: badrun
state: absent
remove: true # Menghapus home directory dan mail spool milik user
Menghapus user dengan opsi remove: true memastikan tidak ada file-file sampah yang tertinggal di direktori /home/ server kita yang dapat mengonsumsi ruang penyimpanan tanpa kita sadari.
Keamanan Password dengan Password Hashing #
Saat kita membuat user yang memerlukan autentikasi password (misalnya untuk akses konsol fisik atau cadangan ketika SSH key tidak dapat diakses), kita tidak boleh menuliskan password dalam bentuk teks biasa (plaintext) di dalam kode Ansible kita. Menulis plaintext password di file playbook adalah pelanggaran keamanan fatal karena siapapun yang memiliki akses ke repositori git kita akan dapat membaca password tersebut.
Linux menyimpan password yang di-hash di dalam file /etc/shadow. Ansible membutuhkan password yang sudah di-crypt menggunakan algoritma hashing yang didukung oleh sistem operasi target (biasanya SHA-512 untuk distro Linux modern). Kita dapat menggunakan filter Python password_hash untuk menghasilkan hash ini secara dinamis pada control node sebelum dikirim ke managed node.
Namun, untuk menggunakan filter ini, pastikan pustaka Python passlib sudah terinstal di control node kita. Kita dapat menginstalnya menggunakan pip.
Berikut perbandingan antara cara yang salah (plaintext) dan cara yang benar menggunakan hashing:
# ANTI-PATTERN: Menyimpan password dalam plaintext di file plays atau inventory
- name: Buat user dengan plaintext password
user:
name: adminuser
password: "MySuperSecretPassword123"
# BENAR: Menggunakan hash SHA-512 dengan salt acak yang aman melalui filter password_hash
- name: Buat user dengan hash password SHA-512 yang aman
user:
name: adminuser
password: "{{ 'MySuperSecretPassword123' | password_hash('sha512', 'mycustomsalt12345') }}"
state: present
Untuk meningkatkan keamanan lebih lanjut, nilai plaintext password sebaiknya tidak ditulis langsung di playbook, melainkan disimpan dalam variabel terenkripsi menggunakan Ansible Vault, lalu dirujuk menggunakan interpolasi variabel:
# vars/main.yml (Sebaiknya dienkripsi dengan ansible-vault)
vault_admin_password: "MySuperSecretPassword123"
# tasks/main.yml
- name: Buat user dengan password dari vault
user:
name: adminuser
password: "{{ vault_admin_password | password_hash('sha512') }}"
state: present
Jika kita tidak menyertakan salt secara manual dalam filter password_hash('sha512'), Ansible akan menghasilkan salt acak secara otomatis. Keunggulannya adalah setiap kali playbook dijalankan, hash yang dihasilkan akan sama jika password-nya tidak berubah, sehingga task ini tetap bersifat idempoten dan tidak akan memicu status changed jika tidak ada perubahan password nyata.
Distribusi SSH Keys secara Aman #
Dalam lingkungan produksi, autentikasi berbasis password untuk protokol SSH harus dinonaktifkan sepenuhnya. Kita harus memaksa seluruh user untuk login menggunakan pasangan kunci SSH (SSH key pair). Ansible mempermudah proses pendistribusian public key milik user ke file ~/.ssh/authorized_keys di managed node melalui module ansible.builtin.authorized_key.
Ada satu parameter yang sangat kuat namun sering diabaikan dalam modul ini, yaitu exclusive. Jika kita menyetel exclusive: true, Ansible tidak hanya akan menambahkan kunci SSH baru yang kita tentukan, tetapi juga akan menghapus kunci SSH lain yang ada di file authorized_keys server target namun tidak terdaftar di playbook kita. Opsi ini sangat krusial untuk audit keamanan, guna memastikan tidak ada kunci SSH “siluman” yang ditambahkan secara manual oleh pihak luar atau mantan administrator.
Berikut adalah implementasi pendistribusian kunci SSH yang aman dan dinamis:
- name: Distribusikan SSH key untuk operator
authorized_key:
user: deployer
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOmG... deployer-key"
state: present
exclusive: true
- name: Distribusikan SSH key untuk tim developer menggunakan file lookup
authorized_key:
user: "{{ item.username }}"
key: "{{ lookup('file', 'ssh_keys/' + item.username + '.pub') }}"
state: present
exclusive: true
loop: "{{ developers_list }}"
loop_control:
label: "{{ item.username }}"
Dalam pola kedua, kita memisahkan data user ke dalam variabel daftar (developers_list) dan membaca file public key dari direktori lokal kita (ssh_keys/alice.pub, ssh_keys/bob.pub, dst.) menggunakan lookup plugin file. Pola ini sangat bersih karena memisahkan logika playbook dari data kunci SSH. Jika kita ingin menambah developer baru, kita cukup meletakkan file .pub mereka di folder tersebut dan menambahkan username mereka ke dalam variabel daftar.
Eskalasi Hak Akses dengan Become #
Secara default, Ansible akan login ke managed node menggunakan user yang kita tentukan di konfigurasi SSH (misalnya user deployer atau ubuntu). Namun, sebagian besar task konfigurasi sistem (seperti menginstal paket atau mengedit konfigurasi di /etc/) memerlukan hak akses administratif (root). Di sinilah fitur become (privilege escalation) masuk.
Kita dapat mengaktifkan become pada tingkat play, block, atau task individu. Kita juga bisa menentukan metode eskalasi (biasanya sudo) dan user tujuan (become_user, defaultnya adalah root).
Mari kita lihat contoh implementasi tingkat eskalasi hak akses:
- name: Playbook konfigurasi sistem server
hosts: all
become: true # Mengaktifkan eskalasi ke root untuk seluruh task di play ini
tasks:
- name: Tulis konfigurasi global yang membutuhkan root
copy:
content: "net.ipv4.ip_forward = 1"
dest: /etc/sysctl.d/99-ip-forward.conf
- name: Jalankan perintah sebagai user database (postgres)
postgresql_db:
name: myapp_db
state: present
become: true
become_user: postgres # Eskalasi khusus ke user postgres, bukan root
Ketika kita menggunakan become: true bersama become_user: postgres, Ansible akan masuk ke server menggunakan SSH user kita, lalu menjalankan perintah sudo untuk beralih identitas menjadi user postgres sebelum menjalankan modul database. Ini mengikuti prinsip hak akses minimum (principle of least privilege) karena task database dijalankan langsung oleh user pemilik database, bukan oleh root.
Menulis Konfigurasi Sudoers secara Aman dan Tervalidasi #
Pemberian hak akses sudo harus dikelola dengan sangat hati-hati. File /etc/sudoers dan file-file di dalam direktori /etc/sudoers.d/ memiliki format sintaksis yang sangat ketat. Kesalahan penulisan satu karakter saja di dalam file sudoers dapat merusak seluruh sistem autentikasi sudo di server target, mengunci akses administratif kita, dan mencegah kita (atau Ansible) untuk memperbaiki kesalahan tersebut.
Untuk menghindari malapetaka ini, kita tidak boleh mengedit file /etc/sudoers secara langsung menggunakan module lineinfile tanpa validasi. Sebagai gantinya, kita harus menulis file konfigurasi terpisah di direktori /etc/sudoers.d/ dan selalu menggunakan parameter validate untuk memeriksa sintaksis file tersebut sebelum ditulis ke tujuan akhir.
Ansible menyediakan mekanisme validasi ini. Jika kita menyertakan parameter validate: 'visudo -cf %s', Ansible akan menulis konten template ke file sementara di server target terlebih dahulu, lalu menjalankan perintah visudo -cf <file_sementara>. Jika perintah tersebut berhasil (exit code 0), Ansible akan menyalin file sementara tersebut ke lokasi tujuan (dest). Jika gagal, Ansible akan menghentikan eksekusi task, menampilkan error, dan membiarkan file asli di server tetap utuh tanpa perubahan.
Mari kita bandingkan anti-pattern dan solusi untuk menulis konfigurasi sudoers:
# ANTI-PATTERN: Mengedit /etc/sudoers langsung dengan lineinfile tanpa validasi
- name: Tambahkan akses sudo untuk deployer
lineinfile:
path: /etc/sudoers
line: "deployer ALL=(ALL) NOPASSWD: ALL"
state: present
# BENAR: Menggunakan template di /etc/sudoers.d/ dengan validasi visudo yang ketat
- name: Deploy file sudoers terpisah untuk deployer
template:
src: templates/sudoers_deployer.j2
dest: /etc/sudoers.d/deployer
owner: root
group: root
mode: '0440' # File sudoers.d WAJIB memiliki permission 0440
validate: 'visudo -cf %s'
Isi dari template templates/sudoers_deployer.j2 dapat kita batasi hanya untuk perintah-perintah spesifik yang diperlukan oleh user tersebut, bukan akses penuh tanpa password ke seluruh perintah:
{# templates/sudoers_deployer.j2 #}
# Memberikan izin terbatas kepada user deployer untuk mengelola service aplikasi
deployer ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx
deployer ALL=(ALL) NOPASSWD: /usr/bin/systemctl reload nginx
deployer ALL=(ALL) NOPASSWD: /usr/bin/systemctl status nginx
Dengan membatasi hak akses sudo hanya pada perintah systemctl untuk nginx, kita mempersempit vektor serangan jika akun deployer tersebut suatu saat disusupi oleh pihak yang tidak bertanggung jawab.
Studi Kasus: Implementasi Lengkap User & Permission Role #
Untuk menyatukan seluruh konsep yang telah kita bahas, mari kita bangun sebuah Ansible role lengkap yang menangani manajemen user, grup, SSH key, dan hak akses sudo secara dinamis dan aman.
Struktur file role kita akan terlihat seperti ini:
roles/user_management/
├── defaults/
│ └── main.yml
├── tasks/
│ └── main.yml
└── templates/
└── sudoers.j2
Berikut adalah isi dari file defaults (roles/user_management/defaults/main.yml) yang mendefinisikan data user dan grup kita:
# defaults/main.yml
---
# Daftar grup kustom yang ingin kita buat
sys_groups:
- { name: "sysadmins", gid: 3000 }
- { name: "devs", gid: 3001 }
- { name: "deployers", gid: 3002 }
# Daftar user yang akan kita kelola siklus hidupnya
sys_users:
- username: "alice"
uid: 3001
primary_group: "sysadmins"
groups: ["sudo"]
shell: "/bin/bash"
password: "$6$rounds=656000$randomsalt$F2e5..." # SHA-512 Hash
ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAliceKey..."
sudo_rules: "ALL=(ALL) NOPASSWD: ALL"
state: "present"
- username: "bob"
uid: 3002
primary_group: "devs"
groups: []
shell: "/bin/bash"
password: "*" # Disable password login, hanya SSH
ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBobKey..."
sudo_rules: "" # Tidak memiliki akses sudo
state: "present"
- username: "olddev"
state: "absent"
Berikut adalah file tasks utama (roles/user_management/tasks/main.yml) yang akan mengeksekusi logika instalasi berdasarkan data di defaults secara idempoten:
# tasks/main.yml
---
- name: Pastikan grup-grup kustom terkonfigurasi di server
group:
name: "{{ item.name }}"
gid: "{{ item.gid }}"
state: present
loop: "{{ sys_groups }}"
loop_control:
label: "{{ item.name }}"
- name: Kelola akun user sistem dan interaktif
user:
name: "{{ item.username }}"
uid: "{{ item.uid | default(omit) }}"
group: "{{ item.primary_group | default(omit) }}"
groups: "{{ item.groups | default([]) }}"
append: true
shell: "{{ item.shell | default('/usr/sbin/nologin') }}"
password: "{{ item.password | default('*') }}"
create_home: "{{ 'true' if item.state | default('present') == 'present' else 'false' }}"
state: "{{ item.state | default('present') }}"
remove: "{{ 'true' if item.state | default('present') == 'absent' else 'false' }}"
loop: "{{ sys_users }}"
loop_control:
label: "{{ item.username }}"
- name: Distribusikan SSH authorized keys secara eksklusif
authorized_key:
user: "{{ item.username }}"
key: "{{ item.ssh_keys | join('\n') }}"
state: present
exclusive: true
loop: "{{ sys_users }}"
loop_control:
label: "{{ item.username }}"
when:
- item.state | default('present') == 'present'
- item.ssh_keys is defined and (item.ssh_keys | length > 0)
- name: Konfigurasi hak akses sudoers untuk user yang memilikinya
template:
src: sudoers.j2
dest: "/etc/sudoers.d/{{ item.username }}"
owner: root
group: root
mode: '0440'
validate: 'visudo -cf %s'
loop: "{{ sys_users }}"
loop_control:
label: "{{ item.username }}"
when:
- item.state | default('present') == 'present'
- item.sudo_rules is defined and (item.sudo_rules | length > 0)
- name: Bersihkan file sudoers untuk user yang dihapus
file:
path: "/etc/sudoers.d/{{ item.username }}"
state: absent
loop: "{{ sys_users }}"
loop_control:
label: "{{ item.username }}"
when: item.state | default('present') == 'absent'
Dan ini adalah template sudoers yang dinamis (roles/user_management/templates/sudoers.j2):
{# templates/sudoers.j2 #}
# File sudoers dikelola secara otomatis oleh Ansible untuk user {{ item.username }}
# Jangan edit file ini secara manual karena perubahan Anda akan ditimpa.
{{ item.username }} {{ item.sudo_rules }}
Dengan menerapkan role ini, kita telah membangun sistem manajemen hak akses dan user yang sepenuhnya deklaratif. Perubahan keanggotaan tim, rotasi kunci SSH, dan modifikasi hak sudo kini semuanya terdokumentasi di repositori Git kita, dapat direview melalui Pull Request, dan dideploy secara aman dengan kepastian validasi sintaksis.
Ringkasan #
- Manajemen User Terpusat — Gunakan Ansible untuk mengotomasi siklus hidup user dan grup demi menghindari inkonsistensi UID/GID dan celah keamanan.
- Grup dengan GID Tetap — Selalu tentukan GID secara eksplisit menggunakan module
groupuntuk memastikan keselarasan hak akses file di seluruh server.- Keamanan Akun Layanan — Gunakan shell
/usr/sbin/nologindan parametersystem: trueuntuk akun non-interaktif guna meminimalkan risiko keamanan.- Penambahan Grup yang Aman — Atur parameter
append: truepada moduleuseragar user tidak terhapus dari grup lain yang sudah diikutinya secara tidak sengaja.- Enkripsi Password SHA-512 — Jangan pernah menyimpan plaintext password di playbook. Gunakan filter
password_hash('sha512')dan amankan nilainya di Ansible Vault.- Audit SSH Key dengan Eksklusif — Aktifkan
exclusive: truedi moduleauthorized_keyuntuk menghapus kunci SSH tidak resmi yang terdaftar di server.- Validasi visudo Sebelum Menulis — Selalu gunakan parameter
validate: 'visudo -cf %s'saat mengedit atau men-deploy file konfigurasi sudoers untuk mencegah kegagalan sistem.- Izin File Sudoers yang Tepat — Terapkan permission
0440pada file di/etc/sudoers.d/agar dibaca dengan benar oleh sistem keamanan Linux.