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

Onlarca, belki yüzlerce sunucuda kullanıcı oluşturmak, SSH key dağıtmak veya eski bir çalışanın erişimini kaldırmak… Manuel yapıyorsan her sunucuya tek tek bağlanıp işlem yapman gerekiyor. Bu hem zaman kaybı hem de hata riski. Ansible tam da bu noktada hayat kurtarıcı oluyor. Bugün gerçek dünya senaryolarıyla toplu kullanıcı yönetimi ve SSH key dağıtımını nasıl otomatize edeceğini anlatacağım.

Temel Kavramlar ve Hazırlık

Ansible’ın bu iş için bize sunduğu iki temel modül var: user ve authorized_key. Bunlara ek olarak group modülünü de sık kullanacağız. Başlamadan önce inventory dosyanın düzgün yapılandırıldığını varsayıyorum.

Basit bir inventory örneğiyle başlayalım:

# /etc/ansible/hosts veya proje dizinindeki inventory.ini

[webservers]
web01 ansible_host=192.168.1.10
web02 ansible_host=192.168.1.11
web03 ansible_host=192.168.1.12

[dbservers]
db01 ansible_host=192.168.1.20
db02 ansible_host=192.168.1.21

[all:vars]
ansible_user=ansible_admin
ansible_ssh_private_key_file=~/.ssh/ansible_key
ansible_become=true
ansible_become_method=sudo

Bu yapıyla devam edeceğiz. Şimdi asıl konuya girelim.

Tekli Kullanıcı Oluşturma: Temelden Başlamak

Karmaşık playbook’lara geçmeden önce temel user modülünü iyi anlamak lazım. Aşağıdaki örnek yeni bir geliştirici kullanıcısı oluşturuyor:

# create_developer.yml
---
- name: Gelistirici kullanicisi olustur
  hosts: webservers
  become: true

  tasks:
    - name: developer grubunu olustur
      group:
        name: developers
        state: present
        gid: 2000

    - name: ali kullanicisini olustur
      user:
        name: ali
        comment: "Ali Yilmaz - Backend Developer"
        uid: 2001
        group: developers
        groups:
          - sudo
          - docker
        shell: /bin/bash
        home: /home/ali
        create_home: true
        password: "{{ 'SecurePassword123!' | password_hash('sha512') }}"
        state: present

    - name: SSH public key ekle
      authorized_key:
        user: ali
        state: present
        key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ... ali@laptop"

Burada dikkat etmeni istediğim birkaç nokta var. uid ve gid sabit vermek, farklı sunucularda dosya sahipliği sorunlarını önlüyor. Özellikle NFS veya shared storage kullanıyorsan bu çok kritik. password alanında hash kullanmak zorundasın, düz metin kabul etmiyor.

Vault ile Şifre Yönetimi

Şifreleri playbook içine açık yazmak en büyük güvenlik hatalarından biri. Ansible Vault bu sorunu çözüyor:

# Vault ile şifrelenmiş değişken dosyası oluştur
ansible-vault create group_vars/all/vault.yml

# Mevcut dosyayı şifrele
ansible-vault encrypt group_vars/all/secrets.yml

# İçerik örneği (şifrelenmeden önce)
vault_user_password: "SuperSecretPass123!"
vault_db_password: "AnotherSecret456!"

Sonra playbook’unda şöyle kullanırsın:

# group_vars/all/vars.yml
users:
  - name: ali
    password: "{{ vault_user_password }}"
    groups: [developers, docker]
    shell: /bin/bash

Playbook’u çalıştırırken:

ansible-playbook -i inventory.ini create_users.yml --ask-vault-pass
# veya vault şifresini dosyadan oku
ansible-playbook -i inventory.ini create_users.yml --vault-password-file ~/.vault_pass

Vault şifre dosyasının izinlerini 600 yapmeyi unutma: chmod 600 ~/.vault_pass

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

Şirkete yeni bir DevOps mühendisi katıldı. Bu kişiye 30 sunucuda erişim vermek gerekiyor. Elle yapsan saatler sürer, Ansible ile dakikalar içinde bitirirsin.

# onboard_engineer.yml
---
- name: Yeni muhendis onboarding
  hosts: all
  become: true
  vars_files:
    - group_vars/all/vault.yml

  vars:
    new_user:
      name: mehmet
      fullname: "Mehmet Kaya - DevOps Engineer"
      uid: 2050
      shell: /bin/bash
      groups_webservers:
        - developers
        - docker
      groups_dbservers:
        - developers
      ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... mehmet@workstation"

  tasks:
    - name: devops grubunu olustur
      group:
        name: devops
        state: present
        gid: 3000

    - name: kullanici olustur
      user:
        name: "{{ new_user.name }}"
        comment: "{{ new_user.fullname }}"
        uid: "{{ new_user.uid }}"
        group: devops
        groups: "{{ new_user.groups_webservers if inventory_hostname in groups['webservers'] else new_user.groups_dbservers }}"
        append: true
        shell: "{{ new_user.shell }}"
        create_home: true
        state: present

    - name: SSH authorized key ekle
      authorized_key:
        user: "{{ new_user.name }}"
        state: present
        key: "{{ new_user.ssh_key }}"
        exclusive: false

    - name: sudo ayarlarini yap (sadece webservers icin)
      copy:
        content: "{{ new_user.name }} ALL=(ALL) NOPASSWD: /usr/bin/docker, /bin/systemctl restart nginxn"
        dest: "/etc/sudoers.d/{{ new_user.name }}"
        mode: '0440'
        validate: /usr/sbin/visudo -cf %s
      when: inventory_hostname in groups['webservers']

append: true parametresi önemli. Bunu yazmazsan mevcut gruplar silinir, sadece belirttiğin gruplar kalır. Yanlışlıkla birinin sudo yetkisini kaldırmak istemezsin.

Toplu Kullanıcı Yönetimi: YAML ile Kullanıcı Listesi

Birden fazla kullanıcıyı aynı anda yönetmek için veri odaklı yaklaşım kullan. Kullanıcı bilgilerini ayrı bir YAML dosyasında tut:

# group_vars/all/users.yml
users:
  - name: ali
    fullname: "Ali Yilmaz"
    uid: 2001
    groups: [developers, docker]
    shell: /bin/bash
    ssh_keys:
      - "ssh-rsa AAAAB3NzaC1yc2E... ali@laptop"
      - "ssh-rsa AAAAB3NzaC1yc2E... ali@home"
    state: present

  - name: ayse
    fullname: "Ayse Demir"
    uid: 2002
    groups: [developers]
    shell: /bin/bash
    ssh_keys:
      - "ssh-ed25519 AAAAC3NzaC1lZ... ayse@macbook"
    state: present

  - name: can
    fullname: "Can Ozturk"
    uid: 2003
    groups: [developers, docker]
    shell: /bin/zsh
    ssh_keys:
      - "ssh-rsa AAAAB3NzaC1yc2E... can@workstation"
    state: present

# Silinecek kullanicilari da burda yonetebilirsin
removed_users:
  - name: eski_calisan
    uid: 1999

Bu listeyi işleyecek playbook:

# manage_users.yml
---
- name: Toplu kullanici yonetimi
  hosts: all
  become: true
  vars_files:
    - group_vars/all/users.yml
    - group_vars/all/vault.yml

  tasks:
    - name: Kullanicilari olustur veya guncelle
      user:
        name: "{{ item.name }}"
        comment: "{{ item.fullname }}"
        uid: "{{ item.uid }}"
        groups: "{{ item.groups }}"
        append: true
        shell: "{{ item.shell }}"
        create_home: true
        state: "{{ item.state }}"
      loop: "{{ users }}"
      loop_control:
        label: "{{ item.name }}"

    - name: SSH key'leri ekle
      authorized_key:
        user: "{{ item.0.name }}"
        state: present
        key: "{{ item.1 }}"
        exclusive: false
      loop: "{{ users | subelements('ssh_keys') }}"
      loop_control:
        label: "{{ item.0.name }} - key ekleniyor"
      when: item.0.state == 'present'

    - name: Ayrilmis kullanicilari kilitli ve archive et
      user:
        name: "{{ item.name }}"
        state: present
        password_lock: true
        shell: /sbin/nologin
        comment: "DISABLED - {{ ansible_date_time.date }}"
      loop: "{{ removed_users }}"
      loop_control:
        label: "{{ item.name }} kilitlendi"

    - name: Ayrilmis kullanicilarin SSH key'lerini temizle
      authorized_key:
        user: "{{ item.name }}"
        state: absent
        key: ""
        exclusive: true
      loop: "{{ removed_users }}"
      loop_control:
        label: "{{ item.name }} SSH key temizleniyor"

subelements filtresi ile her kullanıcının birden fazla SSH key’ini kolayca işleyebiliyoruz. exclusive: true ile de sadece belirtilen key’leri bırakıp gerisini siliyoruz.

Çalışan Ayrılığı Senaryosu: Hızlı Erişim Kaldırma

Bir çalışan şirketten ayrıldı ve erişiminin hemen kaldırılması gerekiyor. Bu durumda paniklemeden şu playbook’u çalıştırırsın:

# offboard_user.yml
---
- name: Kullanici erisimini kaldir
  hosts: all
  become: true

  vars:
    target_user: "{{ offboard_username }}"

  tasks:
    - name: Aktif SSH baglantilari varsa kes
      shell: "pkill -u {{ target_user }} || true"
      ignore_errors: true

    - name: Shell erisimini engelle
      user:
        name: "{{ target_user }}"
        shell: /sbin/nologin
        password_lock: true

    - name: Tum SSH authorized key'leri temizle
      authorized_key:
        user: "{{ target_user }}"
        state: absent
        key: ""
        exclusive: true

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

    - name: Home dizinini yedekle (opsiyonel)
      archive:
        path: "/home/{{ target_user }}"
        dest: "/backup/offboarded/{{ target_user }}_{{ ansible_date_time.date }}.tar.gz"
        mode: '0600'
      ignore_errors: true

    - name: Log kaydi tut
      lineinfile:
        path: /var/log/user_offboard.log
        line: "{{ ansible_date_time.iso8601 }} - {{ target_user }} erisimi kaldirildi - Islem: {{ ansible_user }}"
        create: true

Kullanımı:

ansible-playbook -i inventory.ini offboard_user.yml -e "offboard_username=eski_calisan"

-e ile ekstra değişken geçmek, playbook’u esnek hale getiriyor. Kullanıcı adını her seferinde playbook içine yazman gerekmiyor.

SSH Key Rotasyonu

Güvenlik politikası gereği SSH key’leri periyodik olarak değiştirmek gerekebilir. Özellikle bir key’in ele geçirildiği şüphesi varsa:

# rotate_ssh_keys.yml
---
- name: SSH key rotasyonu
  hosts: all
  become: true
  vars_files:
    - group_vars/all/new_ssh_keys.yml

  tasks:
    - name: Mevcut authorized_keys dosyasini yedekle
      copy:
        src: "/home/{{ item.name }}/.ssh/authorized_keys"
        dest: "/home/{{ item.name }}/.ssh/authorized_keys.bak.{{ ansible_date_time.date }}"
        remote_src: true
        owner: "{{ item.name }}"
        mode: '0600'
      loop: "{{ users_to_rotate }}"
      ignore_errors: true

    - name: Eski keyleri temizle ve yeni keyleri ekle
      authorized_key:
        user: "{{ item.name }}"
        state: present
        key: "{{ item.new_key }}"
        exclusive: true
      loop: "{{ users_to_rotate }}"
      loop_control:
        label: "{{ item.name }} key rotasyonu"

    - name: Rotasyon logunu kaydet
      local_action:
        module: lineinfile
        path: ./key_rotation_log.txt
        line: "{{ ansible_date_time.iso8601 }} - {{ inventory_hostname }} - {{ item.name }} key rotasyonu tamamlandi"
        create: true
      loop: "{{ users_to_rotate }}"
      become: false

exclusive: true burada kritik. Eski key’leri silip sadece yeni key’i bırakıyor. local_action ile de log kaydını kendi makinene alıyorsun, merkezi loglama için kullanışlı.

Rol Tabanlı Yaklaşım: Ansible Roles

Eğer bu işlemleri farklı projelerde tekrar kullanmak istiyorsan, role yapısına geçmeni öneririm:

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

# Oluşan yapı:
# roles/user_management/
# ├── defaults/
# │   └── main.yml
# ├── tasks/
# │   ├── main.yml
# │   ├── create_users.yml
# │   └── manage_ssh_keys.yml
# ├── handlers/
# │   └── main.yml
# └── vars/
#     └── main.yml
# roles/user_management/defaults/main.yml
users: []
removed_users: []
default_shell: /bin/bash
default_groups: []
ssh_key_exclusive: false
backup_home_on_remove: true
backup_dir: /backup/users
# roles/user_management/tasks/main.yml
---
- name: Kullanici gruplarini olustur
  include_tasks: create_groups.yml
  when: user_groups is defined

- name: Kullanicilari yonet
  include_tasks: create_users.yml

- name: SSH key'leri yonet
  include_tasks: manage_ssh_keys.yml

Role’ü çağırmak için ana playbook:

# site.yml
---
- name: Kullanici yonetimi
  hosts: all
  become: true

  roles:
    - role: user_management
      vars:
        users: "{{ developer_users }}"

Role yapısı, kodu modüler ve tekrar kullanılabilir kılıyor. Bir kez yazıyorsun, onlarca projede kullanıyorsun.

Idempotency: Ansible’ın Süper Gücü

Ansible’ın en güzel özelliklerinden biri idempotency, yani aynı playbook’u kaç kez çalıştırsan aynı sonucu vermesi. Kullanıcı zaten varsa tekrar oluşturmaya çalışmıyor, key zaten eklenmişse tekrar eklemiyor. Ama bu bazen beklenmedik durumlar yaratabilir.

Örneğin bir kullanıcının shell’ini değiştirdin ve playbook’ta eski shell yazıyor. Playbook çalışınca shell geri eski haline döner. Bu yüzden playbook’larını tek kaynak gerçek (single source of truth) olarak ele almalısın. Sunucuda manuel değişiklik yapma, her şeyi Ansible üzerinden yönet.

Bunu kontrol etmek için check mode kullanabilirsin:

# Değişiklikleri uygulamadan önce ne olacağını gör
ansible-playbook -i inventory.ini manage_users.yml --check --diff

# Sadece belirli bir task'ı çalıştır
ansible-playbook -i inventory.ini manage_users.yml --tags "ssh_keys"

# Belirli bir host için çalıştır
ansible-playbook -i inventory.ini manage_users.yml --limit web01

--diff parametresi özellikle faydalı. Hangi dosyalarda ne değişeceğini gösteriyor.

Audit ve Raporlama

Kim hangi sunuculara erişebiliyor? Bunu düzenli olarak kontrol etmek güvenlik açısından önemli:

# audit_ssh_access.yml
---
- name: SSH erisim denetimi
  hosts: all
  become: true
  gather_facts: true

  tasks:
    - name: Mevcut kullanicilari listele
      getent:
        database: passwd
      register: system_users

    - name: Her kullanicinin SSH keylerini topla
      slurp:
        src: "/home/{{ item }}/.ssh/authorized_keys"
      loop: "{{ system_users.ansible_facts.getent_passwd.keys() | list }}"
      register: ssh_keys_raw
      ignore_errors: true

    - name: Raporu local'e kaydet
      local_action:
        module: copy
        content: |
          Sunucu: {{ inventory_hostname }}
          Tarih: {{ ansible_date_time.iso8601 }}
          IP: {{ ansible_default_ipv4.address }}
          ===================
          {% for result in ssh_keys_raw.results %}
          {% if result.content is defined %}
          Kullanici: {{ result.item }}
          Keys:
          {{ result.content | b64decode }}
          {% endif %}
          {% endfor %}
        dest: "./audit/{{ inventory_hostname }}_{{ ansible_date_time.date }}.txt"
      become: false

Bu playbook her sunucudaki SSH key durumunu bir rapor dosyasına döküyor. Güvenlik denetimleri için biçilmiş kaftan.

Pratik İpuçları ve Yaygın Hatalar

Uzun yıllar boyunca bu işi yaparken birkaç önemli şey öğrendim:

  • Test ortamında dene: Yeni playbook’ları önce test sunucularında çalıştır, production’da değil.
  • Backup al: Özellikle exclusive: true kullanıyorsan önce authorized_keys dosyasını yedekle.
  • UID sabit tut: Farklı sunucularda aynı kullanıcının farklı UID’si olursa NFS paylaşımlarında sorun çıkar.
  • Vault kullan: Şifreleri asla düz metin olarak playbook’a yazma.
  • Log tut: Her önemli değişikliği logla, kim ne zaman ne yaptı takip edilebilir olsun.
  • İzinleri kontrol et: .ssh dizini 700, authorized_keys 600 olmalı. Ansible genellikle bunu otomatik yapıyor ama kontrol et.
  • Shell değişkenlerinden kaçın: Kullanıcı adları için özel karakter kullanma, sadece harf, rakam ve alt çizgi.

Sık karşılaşılan bir hata da become kullanmadan user modülünü çalıştırmaya çalışmak. Root yetkisi olmadan başka kullanıcılar oluşturamazsın, become: true şart.

Sonuç

Ansible ile kullanıcı ve SSH key yönetimi, özellikle büyük altyapılarda manuel işlemlerin yarattığı kaosa gerçek bir çözüm sunuyor. Yeni çalışan onboarding’den güvenlik denetimlerine, key rotasyonundan acil erişim kapatmaya kadar tüm süreçleri otomatize edebilirsin.

Burada anlattığım yapıyı kendi ortamına uyarlayarak başla. Önce küçük bir kullanıcı listesiyle test et, role yapısına geç, sonra CI/CD pipeline’ına entegre et. HR sisteminden otomatik tetiklenen onboarding playbook’ları, güvenlik ihlalinde tek komutla tüm erişimi kapatan acil playbook’lar… Bunların hepsi mümkün ve fazlasıyla uygulanabilir.

En önemlisi: Playbook’larını Git’te tut. Kim ne zaman hangi kullanıcıya ne yaptı, tüm geçmişi görebilirsin. Bu hem hesap verebilirlik hem de olası hataları geri alma açısından çok değerli. Infrastructure as Code sadece sunucu yapılandırması için değil, kullanıcı yönetimi için de geçerli bir yaklaşım.

Yorum yapın