Hash dosyalarıyla yönetilen bir Postfix kurulumunda her yeni posta hesabı bir SSH oturumu, bir metin düzenleyici ve bir postmap komutu demektir. Yirmi kutuya kadar bu katlanılabilir; elli kutudan sonra hem hata yapma olasılığın artar hem de "şu hesabın şifresini sıfırlar mısın" isteklerinin tamamı sana düşer. MySQL tabanlı Postfix kullanıcı yönetimi tam olarak bu noktada devreye girer: alan adları, kutular ve takma adlar veritabanı tablolarına taşınır, Postfix ve Dovecot bu tabloları sorgular, hesap açma işi ise bir web arayüzüne ya da küçük bir betiğe devredilebilir.
Bu rehberde üç tablodan oluşan sade bir şemayı, Postfix'in mysql: arama dosyalarını, Dovecot'un passdb ve userdb sorgularını, şifrelerin nasıl saklanması gerektiğini ve chroot ile dosya izinleri gibi ilk kurulumda mutlaka takılacağın ayrıntıları anlatacağım. Şemayı bilerek küçük tuttum; çünkü asıl zorluk tablo sayısında değil, sorguların Postfix'in beklediği çıktıyı vermesinde.
Şema: Üç Tablo Yeter#
Karmaşık panel şemalarını bir kenara bırak. Çalışan bir sanal posta altyapısı için üç tablo yeterlidir: alan adları, kutular ve takma adlar. Hepsini tek bir veritabanında topla ve yabancı anahtarlarla birbirine bağla ki bir alan adını sildiğinde ona ait kutular ortada kalmasın.
CREATE DATABASE mailserver CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE mailserver;
CREATE TABLE virtual_domains (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uq_domain (name)
) ENGINE=InnoDB;
CREATE TABLE virtual_users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
domain_id INT UNSIGNED NOT NULL,
email VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL, -- doveadm pw çıktısı
quota_mb INT UNSIGNED NOT NULL DEFAULT 1024,
active TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (id),
UNIQUE KEY uq_email (email),
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE virtual_aliases (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
domain_id INT UNSIGNED NOT NULL,
source VARCHAR(255) NOT NULL,
destination VARCHAR(255) NOT NULL,
active TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (id),
KEY idx_source (source),
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
) ENGINE=InnoDB;
email ve source sütunlarındaki indeksler kritik. Postfix her RCPT TO komutunda bu tabloları sorgular; yoğun bir sunucuda saniyede onlarca sorgu demektir. İndeks olmadan tam tablo taraması yaparsın ve SMTP oturumları yavaşlar. active sütununu da baştan koy: bir hesabı silmek yerine pasife almak, hem yanlışlıkla silmeye karşı sigortadır hem de kotayı aşan hesapları geçici olarak durdurmanı sağlar.
Örnek birkaç kayıtla tabloları dolduralım:
INSERT INTO virtual_domains (name) VALUES ('firmaniz.com'), ('ornek.com');
INSERT INTO virtual_users (domain_id, email, password, quota_mb)
VALUES (1, '[email protected]', '{SHA512-CRYPT}$6$...', 2048);
-- Tek adrese yönlendirme
INSERT INTO virtual_aliases (domain_id, source, destination)
VALUES (1, '[email protected]', '[email protected]');
-- Catch-all
INSERT INTO virtual_aliases (domain_id, source, destination)
VALUES (2, '@ornek.com', '[email protected]');
Postfix Tarafı: mysql Arama Dosyaları#
Postfix veritabanına doğrudan bakmaz; her arama için ayrı bir .cf dosyası okur ve o dosyadaki query satırını çalıştırır. Sorgudaki %s yerine aranan değerin tamamı konur; %u yalnızca kullanıcı kısmını, %d yalnızca alan adı kısmını verir. Üç dosya oluştur:
# /etc/postfix/mysql-virtual-mailbox-domains.cf
user = mailreader
password = guclu-bir-parola
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_domains WHERE name='%s'
# /etc/postfix/mysql-virtual-mailbox-maps.cf
user = mailreader
password = guclu-bir-parola
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_users WHERE email='%s' AND active=1
# /etc/postfix/mysql-virtual-alias-maps.cf
user = mailreader
password = guclu-bir-parola
hosts = 127.0.0.1
dbname = mailserver
query = SELECT destination FROM virtual_aliases WHERE source='%s' AND active=1
Kutu eşlemesinde SELECT 1 yazmam tesadüf değil. Teslimatı Dovecot'a LMTP ile devrettiğinde Postfix'in kutu yolunu bilmesine gerek yoktur; tek ihtiyacı "bu adres var mı" sorusunun cevabıdır. Dönen değer boş olmadığı sürece Postfix adresi kabul eder. Şimdi bu dosyaları main.cf içinde bağla:
# /etc/postfix/main.cf
virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-alias-maps.cf
virtual_transport = lmtp:unix:private/dovecot-lmtp
Alan adı sınıflarının nasıl çalıştığını ve bir alan adının neden yalnızca tek bir listeye yazılabileceğini Postfix virtual domain yapılandırması yazısında ayrıntısıyla ele almıştım; buradaki tek fark, listelerin kaynağının dosya yerine veritabanı olması.
Sorguları Canlıya Almadan Önce Test Etmek#
Postfix, arama dosyalarını komut satırından da sorgulayabilir. Bu, hata ayıklamanın en hızlı yoludur; çünkü SMTP oturumu açmadan, log okumadan doğrudan sorgunun sonucunu görürsün:
# Alan adı tanınıyor mu? (boş olmayan çıktı = evet)
postmap -q firmaniz.com mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
# Kutu var mı?
postmap -q [email protected] mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
# Alias nereye çözülüyor?
postmap -q [email protected] mysql:/etc/postfix/mysql-virtual-alias-maps.cf
# Beklenen çıktı: [email protected]
Çıktı boş geliyorsa sırayla şunları kontrol et: veritabanı kullanıcısının bağlanabildiğini (mysql -u mailreader -p mailserver), sorgunun elle çalıştırıldığında satır döndürdüğünü, active=1 koşulunun kaydı elemediğini ve .cf dosyasının Postfix tarafından okunabildiğini. Sıralama bu; en üsttekinden başlayıp aşağı in.
Dovecot Tarafı: passdb ve userdb#
Postfix "adres var mı" sorusunu cevaplar, kimlik doğrulamayı ve kutu yolunu Dovecot yönetir. Dovecot'un iki ayrı sorguya ihtiyacı var: password_query giriş yapan kullanıcının şifresini doğrular, user_query kutunun diskteki yerini ve kotasını söyler.
# /etc/dovecot/dovecot-sql.conf.ext
driver = mysql
connect = host=127.0.0.1 dbname=mailserver user=mailreader password=guclu-bir-parola
default_pass_scheme = SHA512-CRYPT
password_query = SELECT email AS user, password \
FROM virtual_users WHERE email='%u' AND active=1
user_query = SELECT CONCAT('/var/mail/vhosts/', SUBSTRING_INDEX(email,'@',-1), \
'/', SUBSTRING_INDEX(email,'@',1)) AS home, \
5000 AS uid, 5000 AS gid, \
CONCAT('*:storage=', quota_mb, 'M') AS quota_rule \
FROM virtual_users WHERE email='%u' AND active=1
uid ve gid değerlerini sabit vermem bilinçli: tüm sanal kutuları tek bir sistem kullanıcısı (vmail) yazar, her posta hesabı için ayrı Unix kullanıcısı açılmaz. quota_rule alanı Dovecot'un kota eklentisini besler; kotayı veritabanından okuyunca hesap bazında sınır değiştirmek tek bir UPDATE cümlesine iner.
Şifreleri asla düz metin saklama. Dovecot'un kendi aracıyla hash üret ve tabloya onu yaz:
# Yeni bir hesap için şifre hash'i üret
doveadm pw -s SHA512-CRYPT
# Enter new password: ...
# {SHA512-CRYPT}$6$Xk9...uzun-hash...
# Kimlik doğrulamayı canlıda test et
doveadm auth test [email protected]
# passdb: [email protected] auth succeeded
Rastgele ve güçlü parolalar üretmek için şifre üretici aracımızı kullanabilirsin. Kullanıcıların kendi parolalarını değiştirebilmesi gerekiyorsa, webmail üzerinden bir şifre değiştirme eklentisi bağlaman gerekir; bunun kullanıcı tarafındaki adımlarını e-posta şifresi değiştirme yazısında bulabilirsin.
Güvenlik: Veritabanı Erişimini Daraltmak#
Postfix ve Dovecot bu tablolara yalnızca okumak için erişir. Buna rağmen pek çok kurulumda GRANT ALL PRIVILEGES ile tam yetkili bir kullanıcı tanımlandığını görüyorum. Postfix'in yapılandırma dosyaları ele geçirilirse, bu tek satır farkı "posta adreslerini okuyabildi" ile "tüm veritabanını sildi" arasındaki farktır.
CREATE USER 'mailreader'@'127.0.0.1' IDENTIFIED BY 'guclu-bir-parola';
GRANT SELECT ON mailserver.virtual_domains TO 'mailreader'@'127.0.0.1';
GRANT SELECT ON mailserver.virtual_users TO 'mailreader'@'127.0.0.1';
GRANT SELECT ON mailserver.virtual_aliases TO 'mailreader'@'127.0.0.1';
FLUSH PRIVILEGES;
Hesap açma ve şifre değiştirme işlemleri için ayrı, yazma yetkili bir kullanıcı tanımla ve onu yalnızca yönetim arayüzünde kullan. Dosya izinleri de en az bunun kadar önemli: .cf dosyaları veritabanı parolasını düz metin tutar ve varsayılan izinlerle herkes tarafından okunabilir durumdadır.
chgrp postfix /etc/postfix/mysql-*.cf
chmod 640 /etc/postfix/mysql-*.cf
chown root:dovecot /etc/dovecot/dovecot-sql.conf.ext
chmod 640 /etc/dovecot/dovecot-sql.conf.ext
| Riskli yapılandırma | Neden tehlikeli | Doğrusu |
|---|---|---|
GRANT ALL PRIVILEGES | Okuma yetkisi yeterken silme/yazma açılır | Yalnızca SELECT |
hosts = localhost (soket) | Chroot içinden soket görünmez | hosts = 127.0.0.1 |
.cf dosyası 644 | Parola tüm kullanıcılara açık | 640, grup postfix |
| Düz metin parola sütunu | Veritabanı sızarsa tüm hesaplar açık | SHA512-CRYPT hash |
%u yerine elle birleştirme | SQL enjeksiyonu riski | Postfix değişkenleri |
Sık Yapılan Hatalar ve Tuzaklar#
Chroot ve MySQL soketi. Birçok dağıtımda Postfix'in smtpd süreci /var/spool/postfix içinde chroot edilmiş çalışır. Chroot içinde /var/run/mysqld/mysqld.sock diye bir dosya yoktur; bu yüzden hosts = localhost yazarsan bağlantı sessizce başarısız olur ve tüm alıcılar reddedilir. Çözüm basit: hosts = 127.0.0.1 yazarak TCP üzerinden bağlan.
Sorgunun birden çok satır döndürmesi. virtual_mailbox_maps sorgusu bir adres için birden fazla satır döndürürse Postfix davranışı beklenmedik olur. UNIQUE KEY uq_email kısıtını koymanın sebebi budur; şemayı elle kurduysan bu indeksin var olduğundan emin ol.
Alias'ın kutuyu gölgelemesi. virtual_alias_maps her zaman kutu aramasından önce çalışır. Bir adres için hem alias hem kutu tanımlıysa mesaj kutuya değil, alias hedefine gider. Kutuya mesaj düşmüyorsa ilk bakacağın yer alias tablosudur.
Catch-all'ı yanlış tabloya yazmak. @ornek.com biçimindeki catch-all girdisi virtual_aliases tablosuna aittir, virtual_users tablosuna değil. Kutu tablosuna yazarsan hiçbir işe yaramaz. Catch-all'ın maliyetini de unutma; ayrıntısı catch-all e-posta adresi yazısında.
Yedeklemeyi unutmak. Artık kritik veri diskteki dosyalarda değil, veritabanında. Kutu dizinlerini yedekleyip veritabanını yedeklemezsen, geri dönüşte elinde sahibi belli olmayan bir yığın Maildir kalır.
# Şema + veri yedeği (cron ile günlük çalıştır)
mysqldump --single-transaction --routines mailserver \
| gzip > /yedek/mailserver-$(date +%F).sql.gz
Sıkça Sorulan Sorular#
Postfix için MySQL yerine PostgreSQL kullanabilir miyim#
Evet, Postfix pgsql: öneki ile PostgreSQL tablolarını da sorgular ve arama dosyalarının yapısı neredeyse aynıdır; yalnızca bağlantı satırı ve bazı fonksiyon adları değişir. Dovecot da driver = pgsql ile aynı şemayı kullanabilir. Seçimi genellikle sunucuda hangi veritabanının zaten çalıştığı belirler; ikisi de bu iş yükü için fazlasıyla yeterlidir.
Kullanıcı şifrelerini hangi formatta saklamalıyım#
Günümüz kurulumlarında SHA512-CRYPT veya ARGON2ID makul varsayılanlardır; ikisi de tuzlanmış ve yavaş hash üretir. Hash'i doveadm pw -s SHA512-CRYPT komutuyla üretir, çıktının tamamını (şema önekiyle birlikte) tabloya yazarsın. Düz metin veya basit MD5 saklamaktan kesinlikle kaçın; veritabanı bir gün sızarsa aradaki fark tüm hesapların anında ele geçirilmesi olur.
Veritabanı çökerse posta akışı ne olur#
Postfix alıcı doğrulaması yapamadığı için mesajları geçici hata (4xx) ile erteler, kalıcı olarak reddetmez. Yani gönderen sunucular mesajı kuyruğa alır ve veritabanı geri geldiğinde teslimat kaldığı yerden devam eder; posta kaybı yaşanmaz. Yine de erteleme süresiz değildir, bu yüzden veritabanını mail sunucusuyla aynı makinede tutmak ya da yüksek erişilebilirlik kurmak makul bir tercihtir.
Hesap açtıktan sonra Postfix'i yeniden başlatmam gerekir mi#
Hayır. Veritabanı tabanlı kurulumun en büyük avantajı budur: tabloya bir satır eklediğin anda yeni adres geçerli olur, çünkü Postfix her sorguda veritabanına gider. Hash dosyalarındaki postmap ve postfix reload adımları tamamen ortadan kalkar. Yalnızca .cf dosyalarının kendisini veya main.cf içeriğini değiştirdiğinde postfix reload çalıştırman gerekir.
Kotayı nasıl uygularım ve kullanıcı kotayı aşarsa ne olur#
Kotayı Dovecot'un kota eklentisi uygular; user_query içinde döndürdüğün quota_rule değeri hesap başına sınırı belirler. Kota dolduğunda Dovecot yeni mesajı reddeder ve Postfix gönderene kalıcı bir hata döndürür. Kullanıcıyı önceden uyarmak için Dovecot'un kota uyarı betiklerini yapılandırabilir, yüzde 90 seviyesinde otomatik bilgilendirme maili gönderebilirsin.
Mevcut hash dosyalarımı veritabanına nasıl taşırım#
Taşıma tek seferlik ve düşük riskli bir iştir: vmailbox ve virtual dosyalarını satır satır okuyup INSERT cümlelerine çeviren küçük bir betik yazman yeterli. Şifreler zaten Dovecot tarafında tutuluyorsa onları olduğu gibi kopyalarsın. Geçiş sırasında main.cf içindeki hash: tanımlarını mysql: ile değiştirmeden önce postmap -q ile tüm sorguların doğru cevap verdiğini doğrula, sonra tek seferde geçiş yap.
Kapanış#
Veritabanı tabanlı kullanıcı yönetimi, mail sunucusunu "her değişiklik için SSH gerektiren bir sistem" olmaktan çıkarıp yönetilebilir bir servise dönüştürür. Aklında tutman gereken dört alışkanlık var: şemayı üç tabloyla sade tut ve email ile source sütunlarını indeksle, veritabanı kullanıcısına yalnızca SELECT yetkisi ver, .cf dosyalarını 640 izinle kilitle ve chroot yüzünden soket yerine 127.0.0.1 üzerinden bağlan. Her değişiklikten sonra postmap -q ve doveadm auth test ile sorguları canlıda doğrula.
Bu altyapıyı sıfırdan kurmak yerine hazır ve yapılandırılmış bir sunucuyla başlamak istersen, SMTP sunucu paketlerimiz Postfix ve Dovecot kurulu, rDNS kaydı tanımlı olarak teslim edilir. Kullanıcı yönetimini hiç üstlenmek istemiyorsan kurumsal e-posta çözümlerimiz hesap açma, kota ve yedekleme işini tamamen devralır; kendi sunucunu işletip yalnızca bakımını devretmek istediğinde ise sunucu yönetimi hizmetimiz veritabanı ve mail katmanını birlikte izler.