Python ile Güvenli Script Yazımı: Şifre ve Anahtar Yönetimi

Scriptlerinizde şifreleri düz metin olarak saklıyorsanız, bir gün bu kodu başkasının eline geçtiğini ya da yanlışlıkla bir Git reposuna push’ladığınızı hayal edin. O his gerçekten korkunç. Yıllar içinde pek çok sysadmin meslektaşımın “şimdilik böyle yapalım” diyerek hardcode ettiği şifrelerin nasıl felakete yol açtığını gördüm. Bu yazıda Python scriptlerinizde şifre ve anahtar yönetimini nasıl düzgün yapacağınızı, hangi araçları kullanmanız gerektiğini ve gerçek dünya senaryolarıyla pratik çözümler üretmeyi ele alacağız.

Neden Hardcode Şifre Tehlikelidir?

Klasik sysadmin scripti şöyle görünür:

# YANLIŞ ÖRNEK - Bunu ASLA yapmayin!
db_password = "SuperSecretP@ss123"
api_key = "sk-1234567890abcdef"
ssh_password = "root123"

Bu kodun birkaç dakika sonra GitHub’a gitme ihtimali, ekip arkadaşınız tarafından görülme ihtimali ya da log dosyalarına düşme ihtimali her zaman vardır. Şifreleri kod içinde saklamamak sysadmin dünyasının altın kuralıdır.

Peki ne yapacağız? Birkaç farklı yaklaşım var ve her birinin kullanım senaryosu farklı.

Environment Variable ile Şifre Yönetimi

En basit ve en yaygın yöntem environment variable kullanmaktır. Özellikle CI/CD pipeline’larında ve container ortamlarında bu yaklaşım standarttır.

import os
import sys

def get_db_credentials():
    """Environment variable'dan veritabani bilgilerini al."""
    db_host = os.environ.get("DB_HOST", "localhost")
    db_user = os.environ.get("DB_USER")
    db_password = os.environ.get("DB_PASSWORD")
    
    if not db_user or not db_password:
        print("HATA: DB_USER ve DB_PASSWORD environment variable'lari ayarlanmamis!")
        sys.exit(1)
    
    return {
        "host": db_host,
        "user": db_user,
        "password": db_password
    }

if __name__ == "__main__":
    creds = get_db_credentials()
    print(f"Baglanti yapiliyor: {creds['user']}@{creds['host']}")
    # Şifreyi asla print etmeyin!

Bu scripti çalıştırmadan önce shell’de şu şekilde export yaparsınız:

export DB_HOST="prod-db-01.sirket.local"
export DB_USER="appuser"
export DB_PASSWORD="gercek_sifreniz"
python3 db_backup.py

Ya da .env dosyası kullanıyorsanız python-dotenv kütüphanesi işinizi kolaylaştırır. Ama dikkat edin, .env dosyasını asla Git’e commit etmeyin. .gitignore dosyanıza .env eklemek zorundasınız.

# .env dosyasi ornegi
DB_HOST=prod-db-01.sirket.local
DB_USER=appuser
DB_PASSWORD=gercek_sifreniz
API_KEY=sk-abcdef1234567890
from dotenv import load_dotenv
import os

# .env dosyasini yukle
load_dotenv()

db_password = os.getenv("DB_PASSWORD")
api_key = os.getenv("API_KEY")

print(f"API Key yuklendi: {api_key[:8]}...")  # Sadece ilk 8 karakteri goster

Python Keyring ile Sistem Anahtar Deposu

Linux’ta GNOME Keyring ya da KWallet, macOS’ta Keychain, Windows’ta Credential Manager kullanmak istiyorsanız keyring kütüphanesi mükemmel bir seçenek. Bu yöntem özellikle interaktif araçlar ve geliştirici workstation’ları için idealdir.

pip install keyring
import keyring
import getpass

def save_credentials(service_name, username):
    """Kimlik bilgilerini sistem anahtar deposuna kaydet."""
    password = getpass.getpass(f"{service_name} icin sifre girin: ")
    keyring.set_password(service_name, username, password)
    print(f"Kimlik bilgileri '{service_name}' servisi icin kaydedildi.")

def get_credentials(service_name, username):
    """Sistem anahtar deposundan kimlik bilgilerini al."""
    password = keyring.get_password(service_name, username)
    
    if password is None:
        print(f"Kayitli sifre bulunamadi. Ilk kez calistiriliyor...")
        save_credentials(service_name, username)
        password = keyring.get_password(service_name, username)
    
    return username, password

# Kullanim ornegi
service = "prod-database"
user = "dbadmin"

username, password = get_credentials(service, user)
print(f"Kullanici: {username} icin sifre alindi.")
# password degiskenini direkt kullanin, yazdiırmayin

Bu yöntemin güzelliği şifrenin işletim sisteminin güvenli deposunda şifreli olarak tutulması. Script kodunda hiçbir şekilde şifre bulunmuyor.

Cryptography Kütüphanesi ile Şifreleme

Bazen şifreleri bir dosyada saklamak zorunda kalırsınız, örneğin headless sunucularda ya da batch işlemlerde. Bu durumda cryptography kütüphanesi ile Fernet simetrik şifreleme kullanabilirsiniz.

pip install cryptography
from cryptography.fernet import Fernet
import os
import json
import base64

class SecureCredentialStore:
    """Sifreli kimlik bilgisi deposu."""
    
    def __init__(self, key_file="secret.key", creds_file="credentials.enc"):
        self.key_file = key_file
        self.creds_file = creds_file
        self.key = self._load_or_create_key()
        self.fernet = Fernet(self.key)
    
    def _load_or_create_key(self):
        """Sifrelem anahtarini yukle veya olustur."""
        if os.path.exists(self.key_file):
            with open(self.key_file, "rb") as f:
                return f.read()
        else:
            key = Fernet.generate_key()
            with open(self.key_file, "wb") as f:
                f.write(key)
            # Anahtar dosyasini sadece sahibi okuyabilsin
            os.chmod(self.key_file, 0o600)
            print(f"Yeni sifreeleme anahtari olusturuldu: {self.key_file}")
            return key
    
    def save_credential(self, name, value):
        """Kimlik bilgisini sifreli olarak kaydet."""
        creds = self._load_all_credentials()
        creds[name] = value
        
        encrypted = self.fernet.encrypt(json.dumps(creds).encode())
        with open(self.creds_file, "wb") as f:
            f.write(encrypted)
        os.chmod(self.creds_file, 0o600)
        print(f"'{name}' sifreli olarak kaydedildi.")
    
    def get_credential(self, name):
        """Sifreli depodan kimlik bilgisini al."""
        creds = self._load_all_credentials()
        return creds.get(name)
    
    def _load_all_credentials(self):
        """Tum sifreli kimlik bilgilerini yukle."""
        if not os.path.exists(self.creds_file):
            return {}
        
        with open(self.creds_file, "rb") as f:
            encrypted_data = f.read()
        
        decrypted = self.fernet.decrypt(encrypted_data)
        return json.loads(decrypted.decode())

# Kullanim
store = SecureCredentialStore()
store.save_credential("mysql_password", "ProdSifrem@2024")
store.save_credential("api_key", "sk-abcdef123456")

mysql_pass = store.get_credential("mysql_password")
print(f"MySQL sifresi alindi: {'*' * len(mysql_pass)}")

Önemli uyarı: Bu yöntemde anahtar dosyasını (secret.key) güvende tutmak kritik. Şifreli dosyayı Git’e alabilirsiniz ama anahtar dosyasını kesinlikle almayın.

HashiCorp Vault ile Kurumsal Şifre Yönetimi

Production ortamlarında gerçek bir secrets management çözümü kullanmak gerekir. HashiCorp Vault bu konuda en yaygın tercih. Küçük ve orta ölçekli ortamlar için bile Vault kurulumu karmaşık görünse de hvac Python kütüphanesi ile entegrasyon oldukça basit.

pip install hvac
import hvac
import os

class VaultClient:
    """HashiCorp Vault entegrasyonu."""
    
    def __init__(self):
        vault_addr = os.environ.get("VAULT_ADDR", "http://vault.sirket.local:8200")
        vault_token = os.environ.get("VAULT_TOKEN")
        
        if not vault_token:
            raise ValueError("VAULT_TOKEN environment variable ayarlanmamis!")
        
        self.client = hvac.Client(
            url=vault_addr,
            token=vault_token
        )
        
        if not self.client.is_authenticated():
            raise ConnectionError("Vault kimlik dogrulama basarisiz!")
        
        print("Vault baglantisi basarili.")
    
    def get_secret(self, path, key=None):
        """Vault'tan secret al."""
        try:
            response = self.client.secrets.kv.v2.read_secret_version(
                path=path,
                mount_point="secret"
            )
            data = response["data"]["data"]
            
            if key:
                return data.get(key)
            return data
            
        except hvac.exceptions.InvalidPath:
            print(f"HATA: '{path}' yolunda secret bulunamadi.")
            return None
    
    def write_secret(self, path, secret_data):
        """Vault'a secret yaz."""
        self.client.secrets.kv.v2.create_or_update_secret(
            path=path,
            secret=secret_data,
            mount_point="secret"
        )
        print(f"Secret '{path}' yoluna yazildi.")

# Kullanim ornegi
vault = VaultClient()

# Veritabani bilgilerini al
db_creds = vault.get_secret("database/production")
if db_creds:
    print(f"DB kullanici: {db_creds['username']}")
    # db_creds['password'] ile baglanti kur

# API anahtarini al
api_key = vault.get_secret("api/external-service", key="api_key")

Gerçek Dünya Senaryosu: Otomatik Yedekleme Scripti

Şimdi tüm bu bilgileri birleştirerek gerçekçi bir senaryo oluşturalım. Çok sayıda sunucunun MySQL veritabanlarını yedekleyen ve sonuçları S3’e yükleyen bir script yazalım.

#!/usr/bin/env python3
"""
Guvenli MySQL Yedekleme Scripti
Sifreler environment variable veya keyring'den alinir.
"""

import os
import sys
import subprocess
import logging
import keyring
import getpass
from datetime import datetime
from pathlib import Path

# Logging ayarlari - sifre log'a dusmemeli!
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('/var/log/db_backup.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

class DatabaseBackup:
    def __init__(self):
        self.db_host = os.environ.get("DB_HOST", "localhost")
        self.db_user = os.environ.get("DB_USER", "backup_user")
        self.db_password = self._get_db_password()
        self.s3_bucket = os.environ.get("S3_BUCKET", "sirket-db-backups")
        self.backup_dir = Path("/tmp/db_backups")
        self.backup_dir.mkdir(exist_ok=True)
    
    def _get_db_password(self):
        """
        Sifre alma onceligi:
        1. Environment variable
        2. Keyring (sistem anahtar deposu)
        3. Interaktif giris
        """
        # Once environment variable'a bak
        password = os.environ.get("DB_PASSWORD")
        if password:
            logger.info("Sifre environment variable'dan alindi.")
            return password
        
        # Keyring'e bak
        password = keyring.get_password("mysql-backup", self.db_user)
        if password:
            logger.info("Sifre sistem anahtar deposundan alindi.")
            return password
        
        # Interaktif giris
        logger.warning("Kayitli sifre bulunamadi, interaktif giris gerekli.")
        password = getpass.getpass(f"MySQL {self.db_user} sifresi: ")
        
        # Ileriki kullanimlar icin kaydet
        save = input("Bu sifreyi sistem anahtar deposuna kaydetmek ister misiniz? (e/h): ")
        if save.lower() == 'e':
            keyring.set_password("mysql-backup", self.db_user, password)
            logger.info("Sifre sistem anahtar deposuna kaydedildi.")
        
        return password
    
    def backup_database(self, db_name):
        """Belirtilen veritabanini yedekle."""
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_file = self.backup_dir / f"{db_name}_{timestamp}.sql.gz"
        
        # MYSQL_PWD env variable kullanimi - komut satiri gecmisinde gorunmez
        env = os.environ.copy()
        env["MYSQL_PWD"] = self.db_password
        
        cmd = [
            "mysqldump",
            f"--host={self.db_host}",
            f"--user={self.db_user}",
            "--single-transaction",
            "--routines",
            "--triggers",
            db_name
        ]
        
        try:
            with open(backup_file, 'wb') as f:
                dump_proc = subprocess.Popen(
                    cmd,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    env=env  # Sifre env uzerinden gidecek
                )
                gzip_proc = subprocess.Popen(
                    ["gzip", "-9"],
                    stdin=dump_proc.stdout,
                    stdout=f,
                    stderr=subprocess.PIPE
                )
                dump_proc.stdout.close()
                gzip_proc.communicate()
            
            if dump_proc.returncode == 0:
                logger.info(f"{db_name} yedegi basariyla olusturuldu: {backup_file}")
                return backup_file
            else:
                logger.error(f"{db_name} yedegi basarisiz!")
                return None
                
        except Exception as e:
            logger.error(f"Yedekleme hatasi: {e}")
            return None

if __name__ == "__main__":
    backup = DatabaseBackup()
    databases = ["production_db", "analytics_db", "user_db"]
    
    for db in databases:
        result = backup.backup_database(db)
        if result:
            logger.info(f"Yedek hazir: {result}")

Bu script’in güzel yanı şifrenin hiçbir zaman log dosyasına ya da komut satırı geçmişine düşmemesi. MYSQL_PWD environment variable kullanımı, --password=sifre şeklinde komut satırına geçmekten çok daha güvenlidir.

SSH Anahtarı Yönetimi

Birden fazla sunucuya bağlanan scriptlerde SSH anahtar yönetimi de kritik. paramiko kütüphanesi ile şifreli SSH anahtarlarını nasıl kullanacağınızı görelim.

import paramiko
import os
import getpass
import keyring

def create_ssh_client(hostname, username, port=22):
    """
    Guvenli SSH istemcisi olustur.
    Anahtar dosyasi veya sifre kullanir.
    """
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.RejectPolicy())  # Bilinmeyen host'lari reddet!
    
    # known_hosts dosyasini yukle
    known_hosts = os.path.expanduser("~/.ssh/known_hosts")
    if os.path.exists(known_hosts):
        client.load_host_keys(known_hosts)
    
    # SSH anahtari varsa kullan
    ssh_key_path = os.path.expanduser("~/.ssh/id_rsa")
    
    if os.path.exists(ssh_key_path):
        # Anahtar passphrase'ini keyring'den al
        passphrase = keyring.get_password("ssh-key", ssh_key_path)
        
        try:
            client.connect(
                hostname=hostname,
                port=port,
                username=username,
                key_filename=ssh_key_path,
                passphrase=passphrase,
                look_for_keys=False,
                allow_agent=False
            )
            print(f"SSH baglantisi kuruldu: {username}@{hostname}")
            return client
            
        except paramiko.ssh_exception.PasswordRequiredException:
            # Passphrase gerekiyor, interaktif sor
            passphrase = getpass.getpass(f"SSH anahtar passphrase'i ({ssh_key_path}): ")
            keyring.set_password("ssh-key", ssh_key_path, passphrase)
            
            client.connect(
                hostname=hostname,
                port=port,
                username=username,
                key_filename=ssh_key_path,
                passphrase=passphrase
            )
            return client
    
    raise ConnectionError(f"SSH anahtari bulunamadi: {ssh_key_path}")

# Kullanim
try:
    ssh = create_ssh_client("prod-web-01.sirket.local", "deploy")
    stdin, stdout, stderr = ssh.exec_command("df -h")
    print(stdout.read().decode())
    ssh.close()
except Exception as e:
    print(f"SSH hatasi: {e}")

Şifre Doğruluğunu Test Etme ve Maskeleme

Scriptlerinizin çıktılarında şifrelerin yanlışlıkla görünmesini engellemek için bir yardımcı sınıf oluşturalım:

import re
import logging

class SensitiveDataFilter(logging.Filter):
    """Log kayitlarindan hassas verileri temizler."""
    
    SENSITIVE_PATTERNS = [
        (r'password["s]*[:=]["s]*S+', 'password=***'),
        (r'passwd["s]*[:=]["s]*S+', 'passwd=***'),
        (r'secret["s]*[:=]["s]*S+', 'secret=***'),
        (r'token["s]*[:=]["s]*S+', 'token=***'),
        (r'api[_-]?key["s]*[:=]["s]*S+', 'api_key=***'),
        (r'sk-[a-zA-Z0-9]{20,}', 'sk-***'),  # OpenAI tarzı key'ler
    ]
    
    def filter(self, record):
        record.msg = self._mask_sensitive_data(str(record.msg))
        if record.args:
            record.args = tuple(
                self._mask_sensitive_data(str(arg)) if isinstance(arg, str) else arg
                for arg in record.args
            )
        return True
    
    def _mask_sensitive_data(self, text):
        for pattern, replacement in self.SENSITIVE_PATTERNS:
            text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
        return text

# Logger'a filtre ekle
logger = logging.getLogger()
logger.addFilter(SensitiveDataFilter())

# Test
logger.info("Baglanti: password=SuperSecret123 ile kuruldu")
# Cikti: Baglanti: password=*** ile kuruldu
logger.info("API key=sk-abcdef1234567890 kullaniliyor")
# Cikti: API key=*** kullaniliyor

En İyi Pratikler ve Hatırlatmalar

Tüm bu yöntemlerin yanında günlük pratik hayatta dikkat etmeniz gereken bazı temel kurallar var:

  • .gitignore zorunlu: .env, *.key, credentials.enc, secrets.yaml gibi dosyalar mutlaka .gitignore‘da olmalı
  • Dosya izinleri: Şifre içeren dosyalar chmod 600 ile sadece sahibine okunabilir olmalı
  • Şifreyi asla print etmeyin: Log ve print ifadelerinde şifreyi doğrudan yazmayın, maskeleme kullanın
  • subprocess’te komut satırından şifre geçirmeyin: --password=sifre yerine environment variable ya da stdin kullanın
  • Düzenli rotasyon: Scriptlerin kullandığı şifreleri ve API anahtarlarını belirli aralıklarla değiştirin
  • Minimum yetki prensibi: Backup user’ın sadece SELECT yetkisi olsun, admin yetkisi olmasın
  • Secrets scanning: Git hook’larına git-secrets ya da truffleHog ekleyin, yanlışlıkla commit’i engelleyin
  • Production ve test ayrımı: Farklı ortamlar için farklı credential’lar kullanın, production şifresi test scriptinde bulunmasın

Sonuç

Python scriptlerinde şifre yönetimi “sonra düzeltirim” diyerek ertelenen ama gerçekten kritik bir konu. Basit bir cron job bile production veritabanına bağlanıyorsa doğru yönetilmelidir.

Küçük ve tek kullanıcılı ortamlar için environment variable ve keyring kombinasyonu çoğunlukla yeterli. Takım ortamlarında .env dosyalarını şifreli bir password manager’da saklayın ve paylaşın. Kurumsal ve büyük ölçekli ortamlarda HashiCorp Vault ya da AWS Secrets Manager, Azure Key Vault gibi managed servislere yatırım yapın.

Unutmayın, bir şifrenin sızdığı an onu ele geçiren kişinin sisteminizde ne kadar süre gezindiğini bilemezsiniz. Güvenlik her zaman “sonra” yapılacak bir şey değil, scriptinizi yazarken düşünmeniz gereken bir tasarım kararıdır. Bugün 15 dakika harcayarak doğru yöntemi uygularsanız, ileride saatler süren incident müdahalesinden kurtulmuş olursunuz.

Yorum yapın