Her Yeni Sunucuda Çalıştırdığım Ansible Playbook'u
Her yeni sunucuda geçtiğim checklist hakkında daha önce yazmıştım: non-root bir kullanıcı, sadece key ile SSH, default-deny bir firewall, Fail2ban, unattended upgrade'ler. Bunu elle yapmak bir saatten az sürüyor ve zor değil. Ama "her sunucu için elle, bir saatten az" ikiden üçten fazla sunucu yönettiğinizde hızla birikiyor, ve küçük tutarsızlıkların sızdığı yer de tam olarak manuel adımlar, bir sunucuya MaxAuthTries 3 giriyor, diğerine girmiyor, çünkü o günü kuran kişinin acelesi vardı.
Çözüm, checklist'i bir playbook'a çevirmek. Her seferinde aynı adımlar, aynı sıra, ve zaten configure edilmiş bir sunucuda tekrar çalıştırıldığında hiçbir şeyi değiştirmeyecek kadar idempotent.
Playbook
---
- name: Baseline hardening for a new server
hosts: new_servers
become: true
vars:
admin_user: deploy
ssh_public_key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
tasks:
- name: Create admin user
user:
name: "{{ admin_user }}"
groups: sudo
shell: /bin/bash
create_home: true
- name: Add SSH key for admin user
authorized_key:
user: "{{ admin_user }}"
key: "{{ ssh_public_key }}"
- name: Disable root login and password auth
lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
loop:
- { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
- { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
- { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' }
notify: restart sshd
- name: Install UFW and Fail2ban
apt:
name: [ufw, fail2ban]
state: present
update_cache: true
- name: Configure UFW defaults
ufw:
direction: "{{ item.direction }}"
policy: "{{ item.policy }}"
loop:
- { direction: incoming, policy: deny }
- { direction: outgoing, policy: allow }
- name: Allow required ports
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop: ['22', '80', '443']
- name: Enable UFW
ufw:
state: enabled
- name: Enable Fail2ban
systemd:
name: fail2ban
enabled: true
state: started
- name: Install unattended-upgrades
apt:
name: unattended-upgrades
state: present
handlers:
- name: restart sshd
systemd:
name: ssh
state: restarted
Çalıştırmak
Yeni bir sunucu inventory'e ekleniyor ve playbook sadece o host'a karşı çalışıyor:
ansible-playbook -i inventory.ini baseline.yml -l new-server-01
İlk çalıştırma tüm işi yapıyor, kullanıcıyı oluşturuyor, SSH'ı kilitliyor, firewall'u kuruyor. Handler, sshd'i sadece config gerçekten değiştiyse restart ediyor, yani bunun olduğu tek an ilk çalıştırma, sonraki her çalıştırma hiçbir şeyin kaymadığını onaylayan bir no-op.
Asıl mesele neden idempotency
Buradaki gerçek değer ilk gün kazanılan zaman değil, komutları elle yazmak yavaş değil zaten. Mesele, altı ay sonra, belirli bir sunucunun tam işlemden geçip geçmediğinden ya da bir incident sırasında aceleyle kurulup kurulmadığından emin olmadığımda, playbook'u tekrar çalıştırıp öğrenebilmem. Her şey zaten yerindeyse, Ansible sıfır değişiklik raporluyor ve devam ediyorum. Bir şey eksikse, dört beş manuel adımdan hangisinin atlandığını hatırlamaya gerek kalmadan yerinde düzeltiliyor.
Bu playbook bilerek küçük. Uygulama stack'leri kurmuyor ya da projeye özel bir şey configure etmiyor, üzerine başka her şeyin katmanlandığı zemin bu. Uygulama playbook'larından ayrı tutmak onu stabil tutuyor, ve sık değişmeyen bir baseline, her seferinde yeniden okumaya gerek kalmadan güvenebileceğiniz bir şey.