Ansible ile Kullanıcı ve SSH Key Yönetimi

Onlarca, belki yüzlerce sunucuda kullanıcı oluşturmak, SSH key dağıtmak, eski kullanıcıları temizlemek… Bunları elle yapmaya çalışan her sysadmin’in er ya da geç saçını yolduğunu biliriz. Bir sunucuya eklemeyi unutursun, başka birinde yanlış bir key girersin, üçüncüsünde kullanıcı grubu eksik kalır. Ansible tam da bu kaos için var. Bu yazıda gerçek dünya senaryoları üzerinden Ansible ile kullanıcı ve SSH key yönetimini adım adım ele alacağız.

Neden Ansible ile SSH Key Yönetimi?

Geleneksel yöntemde her sunucuya SSH ile bağlanıp useradd, passwd, ssh-copy-id komutlarını çalıştırırsın. 5 sunucu için katlanılabilir, 50 sunucu için işkence, 500 sunucu için ise mümkün değil. Ansible’ın buradaki gücü şu: bir playbook yaz, hangi sunucularda çalışacağını tanımla, çalıştır ve uyu.

Bunun ötesinde idempotency özelliği kritik. Aynı playbook’u kaç kez çalıştırırsan çalıştır, sonuç değişmez. Kullanıcı zaten varsa tekrar oluşturmaz, key zaten eklenmişse tekrar eklemez. Bu hem güvenli hem de hata payını minimuma indirir.

Ortam Hazırlığı

Başlamadan önce temel bir inventory ve ansible.cfg dosyamızın olduğunu varsayalım.

# /etc/ansible/hosts veya proje dizinindeki inventory dosyan
[webservers]
web01.sirket.com
web02.sirket.com
web03.sirket.com

[dbservers]
db01.sirket.com
db02.sirket.com

[all:vars]
ansible_user=ansible_admin
ansible_ssh_private_key_file=~/.ssh/ansible_master_key
# ansible.cfg
[defaults]
inventory = ./inventory
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
deprecation_warnings = False

[privilege_escalation]
become = True
become_method = sudo
become_user = root

host_key_checking = False ayarını production’da dikkatli kullan. Yeni sunucular eklerken işe yarar ama tamamen kapalı tutmak güvenlik açısından tartışmalıdır. Bunu inventory’ye sunucu ilk eklendiğinde kullan, sonra açık bırak.

Kullanıcı Oluşturma: Temel Yapı

Ansible’da kullanıcı yönetiminin kalbi user modülüdür. Basit bir örnekle başlayalım.

# create_users.yml
---
- name: Kullanici ve SSH key yonetimi
  hosts: all
  become: yes

  vars:
    users:
      - name: ahmet
        comment: "Ahmet Yilmaz - DevOps"
        groups: ["sudo", "docker"]
        shell: /bin/bash
        state: present
      - name: zeynep
        comment: "Zeynep Kaya - Backend Dev"
        groups: ["developers"]
        shell: /bin/bash
        state: present
      - name: eski_calisan
        state: absent

  tasks:
    - name: Kullanicilari olustur veya sil
      user:
        name: "{{ item.name }}"
        comment: "{{ item.comment | default('') }}"
        groups: "{{ item.groups | default([]) }}"
        shell: "{{ item.shell | default('/bin/bash') }}"
        state: "{{ item.state | default('present') }}"
        create_home: yes
        append: yes
      loop: "{{ users }}"
      when: item.state | default('present') == 'present' or item.state == 'absent'

Burada append: yes önemli. Bu olmadan kullanıcıyı belirttiğin gruplara eklerken mevcut gruplardan çıkarır. Yani bir kullanıcı www-data grubundaysa ve sen onu docker grubuna ekliyorsan, append: no ile www-data‘dan çıkar. Çoğu zaman istemediğin bu davranış.

SSH Key Dağıtımı

Kullanıcıları oluşturduktan sonra SSH key’leri yerleştirme zamanı. authorized_key modülü bu iş için biçilmiş kaftan.

# ssh_keys.yml
---
- name: SSH key dagitimu
  hosts: all
  become: yes

  vars:
    ssh_keys:
      - user: ahmet
        key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBkKk... ahmet@laptop"
        state: present
      - user: ahmet
        key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOther... ahmet@work"
        state: present
      - user: zeynep
        key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB... zeynep@macbook"
        state: present

  tasks:
    - name: SSH authorized key ekle
      authorized_key:
        user: "{{ item.user }}"
        key: "{{ item.key }}"
        state: "{{ item.state | default('present') }}"
        exclusive: no
      loop: "{{ ssh_keys }}"

exclusive: no ile mevcut keyler korunur, sadece yenileri eklenir. Eğer exclusive: yes kullanırsan o kullanıcının tüm keylerini silip yalnızca belirttiğini bırakır. Bu bazen işine yarayabilir, özellikle tam key rotasyonu yapacaksan.

Key’leri Dosyadan Okumak

Key’leri playbook içine gömmek iyi bir pratik değil. Bunun yerine dosyadan oku:

# Yerel makinedeki public key dosyasindan oku
- name: Lokal public key ile SSH erisimi ver
  authorized_key:
    user: "{{ item }}"
    key: "{{ lookup('file', '~/.ssh/keys/' + item + '.pub') }}"
    state: present
  loop:
    - ahmet
    - zeynep
    - mehmet

Bu yaklaşımda ~/.ssh/keys/ klasörüne her kullanıcı için ahmet.pub, zeynep.pub gibi dosyalar koyarsın. Düzenli, takip edilebilir ve git’e alabileceğin bir yapı.

Grup Yönetimi

Kullanıcı oluşturmadan önce grupların var olduğundan emin olmak gerekir, yoksa task hata verir.

# groups_and_users.yml
---
- name: Grup ve kullanici yonetimi
  hosts: all
  become: yes

  vars:
    groups_to_create:
      - name: developers
        gid: 2000
      - name: devops
        gid: 2001
      - name: readonly
        gid: 2002

  tasks:
    - name: Gruplari olustur
      group:
        name: "{{ item.name }}"
        gid: "{{ item.gid | default(omit) }}"
        state: present
      loop: "{{ groups_to_create }}"

    - name: Sudo yetkisi ver (sudoers.d)
      copy:
        content: "%devops ALL=(ALL) NOPASSWD: ALLn"
        dest: /etc/sudoers.d/devops
        owner: root
        group: root
        mode: '0440'
        validate: 'visudo -cf %s'

validate: 'visudo -cf %s' kısmı kritik. Dosyayı yazmadan önce sözdizimini kontrol eder. Hatalı bir sudoers dosyası sistemden kilitlenmen anlamına gelebilir.

Gerçek Dünya Senaryosu: Yeni Çalışan Onboarding

Bir şirkette yeni geliştirici işe girdiğinde yapılacaklar listesi uzun olur. Bunu tam anlamıyla otomatize edelim.

# onboard_developer.yml
---
- name: Yeni gelistirici onboarding
  hosts: webservers:dbservers
  become: yes

  vars_files:
    - vars/new_developer.yml

  tasks:
    - name: Kullaniciyi olustur
      user:
        name: "{{ developer.username }}"
        comment: "{{ developer.full_name }}"
        shell: /bin/bash
        groups: "{{ developer.groups }}"
        append: yes
        create_home: yes
        state: present

    - name: SSH home dizini olustur
      file:
        path: "/home/{{ developer.username }}/.ssh"
        state: directory
        owner: "{{ developer.username }}"
        group: "{{ developer.username }}"
        mode: '0700'

    - name: SSH public key ekle
      authorized_key:
        user: "{{ developer.username }}"
        key: "{{ developer.ssh_public_key }}"
        state: present
        exclusive: no

    - name: Bashrc ozelleştirmesi
      copy:
        src: templates/bashrc_developer
        dest: "/home/{{ developer.username }}/.bashrc"
        owner: "{{ developer.username }}"
        group: "{{ developer.username }}"
        mode: '0644'

    - name: Bilgilendirme mesaji
      debug:
        msg: "{{ developer.full_name }} icin hesap basariyla olusturuldu. Sunucular: {{ ansible_hostname }}"
# vars/new_developer.yml
developer:
  username: can_demir
  full_name: "Can Demir"
  email: "[email protected]"
  groups:
    - developers
    - docker
  ssh_public_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... can@laptop"

Bu yaklaşımla yeni çalışan için sadece vars/new_developer.yml dosyasını dolduruyorsun ve playbook’u çalıştırıyorsun. İK’dan maile bile gerek yok, git’e pull request aç, review geçsin, pipeline çalıştırsın.

Gerçek Dünya Senaryosu: Çalışan Ayrılığı

Bu senaryo güvenlik açısından daha kritik. Birisi işten ayrıldığında erişiminin anında kesilmesi şart.

# offboard_user.yml
---
- name: Kullanici erisimini kes
  hosts: all
  become: yes

  vars:
    departing_user: "eski_calisan"
    archive_home: yes

  tasks:
    - name: SSH keylerini temizle
      authorized_key:
        user: "{{ departing_user }}"
        key: ""
        state: absent
        exclusive: yes
      ignore_errors: yes

    - name: Kullaniciyi kilitle (silme, arsivle)
      user:
        name: "{{ departing_user }}"
        shell: /sbin/nologin
        password_lock: yes
        state: present

    - name: Home dizinini arsivle
      archive:
        path: "/home/{{ departing_user }}"
        dest: "/var/backups/users/{{ departing_user }}_{{ ansible_date_time.date }}.tar.gz"
        format: gz
      when: archive_home

    - name: Aktif oturumu sonlandir
      shell: "pkill -u {{ departing_user }} || true"
      ignore_errors: yes

    - name: Sudo yetkisini kaldir
      file:
        path: "/etc/sudoers.d/{{ departing_user }}"
        state: absent

Kullanıcıyı direkt silmek yerine kilitleme ve arşivleme stratejisi daha sağlıklı. Hukuki süreçler veya teknik inceleme gerekirse veriler kaybolmaz.

Vault ile Güvenli Key Saklama

SSH public keyler zaten halka açık olduğundan vault gerekmez ama private bilgiler, parolalar veya hassas değişkenler için Ansible Vault kullanmak şart.

# Vault ile şifreli değişken dosyası oluştur
ansible-vault create vars/secrets.yml

# Mevcut dosyayı şifrele
ansible-vault encrypt vars/new_developer.yml

# Şifreli dosyayı düzenle
ansible-vault edit vars/secrets.yml

# Playbook'u vault şifresiyle çalıştır
ansible-playbook onboard_developer.yml --ask-vault-pass

# Ya da vault şifre dosyasıyla
ansible-playbook onboard_developer.yml --vault-password-file ~/.vault_pass
# vars/secrets.yml (vault ile şifrelenmiş hali düz gösterim)
admin_password_hash: "$6$rounds=656000$..."
deploy_key_private: |
  -----BEGIN OPENSSH PRIVATE KEY-----
  ...
  -----END OPENSSH PRIVATE KEY-----

SSH Yapılandırması: sshd_config Güçlendirme

Kullanıcı yönetimiyle birlikte SSH servisinin güvenliğini de playbook’a dahil etmek mantıklı.

# harden_ssh.yml
---
- name: SSH guclendir
  hosts: all
  become: yes

  vars:
    ssh_config:
      PermitRootLogin: "no"
      PasswordAuthentication: "no"
      PubkeyAuthentication: "yes"
      X11Forwarding: "no"
      MaxAuthTries: "3"
      LoginGraceTime: "30"
      AllowGroups: "ssh_users devops"
      Protocol: "2"

  tasks:
    - name: SSH grubunu olustur
      group:
        name: ssh_users
        state: present

    - name: sshd_config guncelle
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "^#?{{ item.key }}"
        line: "{{ item.key }} {{ item.value }}"
        state: present
        backup: yes
      loop: "{{ ssh_config | dict2items }}"
      notify: sshd yeniden baslat

    - name: sshd_config syntax kontrolu
      command: sshd -t
      changed_when: false

  handlers:
    - name: sshd yeniden baslat
      service:
        name: sshd
        state: restarted

AllowGroups ile sadece belirttiğin gruplardaki kullanıcıların SSH erişimine izin veriyorsun. Bu çok güçlü bir kısıtlama mekanizması. Kullanıcı sistemde var ama ssh_users grubunda değilse giremez.

Toplu Key Rotasyonu

Güvenlik olayı yaşandığında veya belirli periyotlarda tüm SSH keyleri rotasyona sokmak gerekebilir.

# rotate_ssh_keys.yml
---
- name: SSH key rotasyonu
  hosts: all
  become: yes

  vars:
    rotation_date: "{{ ansible_date_time.date }}"
    users_to_rotate:
      - username: ahmet
        new_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAInew... ahmet@new-laptop"
      - username: zeynep
        new_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIother... zeynep@new-mac"

  tasks:
    - name: Eski keyleri yedekle
      fetch:
        src: "/home/{{ item.username }}/.ssh/authorized_keys"
        dest: "./key_backups/{{ rotation_date }}/{{ item.username }}_{{ inventory_hostname }}_authorized_keys"
        flat: no
      loop: "{{ users_to_rotate }}"
      ignore_errors: yes

    - name: Yeni keyi exclusive olarak yaz
      authorized_key:
        user: "{{ item.username }}"
        key: "{{ item.new_key }}"
        state: present
        exclusive: yes
      loop: "{{ users_to_rotate }}"

fetch modülüyle eski keyleri Ansible çalıştıran makinene çekiyorsun. Bir şeyler ters giderse geri döndürebilirsin.

Role Yapısı ile Modüler Yaklaşım

Büyüyen projelerde playbook’ları tek dosyada tutmak yönetilemez hale gelir. Role yapısı kullan:

# Role yapısını oluştur
ansible-galaxy init roles/user_management

# Oluşan yapı:
roles/user_management/
  tasks/
    main.yml
    create_users.yml
    manage_keys.yml
    cleanup.yml
  vars/
    main.yml
  defaults/
    main.yml
  handlers/
    main.yml
  templates/
    sudoers.j2
# roles/user_management/tasks/main.yml
---
- include_tasks: create_users.yml
  tags: users

- include_tasks: manage_keys.yml
  tags: keys

- include_tasks: cleanup.yml
  tags: cleanup
  when: cleanup_enabled | default(false)
# roles/user_management/defaults/main.yml
---
default_shell: /bin/bash
default_groups: []
cleanup_enabled: false
key_exclusive: false
archive_home_on_delete: true
# site.yml - Ana playbook
---
- name: Kullanici yonetimi
  hosts: all
  become: yes
  roles:
    - role: user_management
      vars:
        users: "{{ lookup('file', 'users.json') | from_json }}"

CI/CD Pipeline Entegrasyonu

GitLab CI veya GitHub Actions ile bu playbook’ları otomatize etmek kullanıcı yönetimini tamamen koda döker.

# .gitlab-ci.yml
stages:
  - validate
  - deploy

variables:
  ANSIBLE_HOST_KEY_CHECKING: "False"

validate_playbook:
  stage: validate
  image: cytopia/ansible:latest
  script:
    - ansible-playbook --syntax-check onboard_developer.yml
    - ansible-lint onboard_developer.yml
  only:
    - merge_requests

deploy_user_changes:
  stage: deploy
  image: cytopia/ansible:latest
  before_script:
    - eval $(ssh-agent -s)
    - echo "$ANSIBLE_SSH_KEY" | ssh-add -
    - mkdir -p ~/.ssh
    - echo "$VAULT_PASSWORD" > ~/.vault_pass
  script:
    - ansible-playbook
        --vault-password-file ~/.vault_pass
        -i inventory/production
        onboard_developer.yml
  only:
    - main
  environment:
    name: production

Bu yapıyla yeni çalışan için git’e PR açılır, review geçer, main’e merge edilir ve pipeline otomatik olarak tüm sunucularda hesabı oluşturur. Tamamen hands-off bir süreç.

Hata Ayıklama ve Doğrulama

Playbook çalıştırdıktan sonra gerçekten işe yarayıp yaramadığını doğrulamak için:

# Dry-run (check mode) ile ne olacağını gör
ansible-playbook create_users.yml --check --diff

# Sadece belirli tag'leri çalıştır
ansible-playbook site.yml --tags "users,keys"

# Belirli sunucularda test et
ansible-playbook create_users.yml --limit web01.sirket.com

# Kullanıcı var mı kontrol et
ansible all -m command -a "id ahmet" -i inventory

# Authorized key kontrol
ansible all -m command -a "cat /home/ahmet/.ssh/authorized_keys" -i inventory

# Verbose mod ile detaylı çıktı
ansible-playbook create_users.yml -vvv

--check --diff kombinasyonu production’a dokunmadan önce mutlaka kullanman gereken şey. Neyin değişeceğini önceden görürsün.

İzleme ve Denetim

Kimin ne zaman erişim aldığını veya kaybettiğini kayıt altına almak compliance açısından önemli.

# Degisiklikleri logla
- name: Kullanici islemini kayit altina al
  lineinfile:
    path: /var/log/ansible_user_changes.log
    line: "{{ ansible_date_time.iso8601 }} | {{ inventory_hostname }} | user={{ item.name }} | action={{ item.state | default('present') }} | triggered_by={{ lookup('env', 'USER') }}"
    create: yes
  loop: "{{ users }}"

Bu log dosyasını merkezi bir log yönetim sistemine (Graylog, ELK stack) göndermek her şeyi kayıt altına alır.

Sonuç

Ansible ile kullanıcı ve SSH key yönetimi, sysadmin’in hayatını kökten değiştiren konulardan biri. Artık “hangi sunucuya ekledim, hangisine eklemedim” kaygısı yok. Playbook çalıştı mı, her yerde tutarlı.

Özetlemek gerekirse:

  • Kullanıcı oluşturma: user modülü, append: yes ile mevcut grupları koru
  • Key dağıtımı: authorized_key modülü, keyleri dosyadan lookup ile oku
  • Güvenlik: exclusive: yes ile key rotasyonu, vault ile hassas veri koruma
  • Offboarding: Silme yerine kilitleme ve arşivleme
  • SSH güçlendirme: lineinfile ile sshd_config yönetimi
  • Ölçeklendirme: Role yapısı ve CI/CD entegrasyonu

En kritik nokta şu: Bu playbook’ları git’te tut, değişiklikleri PR ile yönet, pipeline’a bağla. Kullanıcı yönetimi bir operasyon görevi olmaktan çıkıp bir kod inceleme sürecine dönüştüğünde hem güvenlik hem de denetlenebilirlik katlanarak artar. Altyapını kod olarak yönetmenin en somut ve hemen fayda gören alanlarından biri bu. Dene, göreceksin.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir