Önce bir düzeltme: Bu konunun Türkçe başlığındaki "sabit limitleri" ifadesi hatalı bir çeviri. Hata, disk kotasının sabit limitiyle (hard limit) değil, sabit bağlantılarla (hard link), yani birden fazla adı olan dosyalarla ilgilidir.
DirectAdmin, bir kullanıcının yedeğini almadan önce arşive girecek verileri tarar ve sabit bağlantı bulursa o kullanıcının yedeğini iptal eder. Bütün yöneticilere bildirim gider, kullanıcının arşivi oluşmaz ve neden ortadan kalkana kadar her zamanlanmış yedek aynı şekilde başarısız olur. Kendi sunucularımızda aşağıdaki sırayı izliyoruz.
Kısa cevap
Önce bul, sonra düzelt, en son karar ver.
- Dosyaları listeleyin: find /home/KULLANICI -xdev -type f -links +1 -ls
- Kaynağı düzeltin: Dovecot e-postaları varsayılan olarak sabit bağlantıyla kopyalar (maildir_copy_with_hardlinks = no bunu durdurur), bazı Node.js ve Python araçları önbellekteki dosyalara sabit bağlantı oluşturur, anlık görüntülerin (snapshot) yeri de /home dışıdır.
- Var olan bağlantıları ayırın: her dosyayı kopyalayın, sonra kopyayı asıl adın üzerine taşıyın.
- Kontrolü (backup_hard_link_check=0) yalnızca strict_backup_permissions=1 ve fs.protected_hardlinks=1 etkinse ve hiçbir kullanıcının ev dizininde sahibi olmadığı bir dosyaya bağlantı yoksa kapatın.
- Deneme yedeği ve geri yükleme testiyle bitirin.
Hata mesajı ne söylüyor?
DirectAdmin'in güncel belgelerinde mesaj şöyle geçiyor:
Code:
Backup Abort (2): A hard link was found under User username's path
| Sayı | Bölüm (DirectAdmin belgeleri) | İlk bakılacak yer |
|---|---|---|
| 1 | ~/Maildir | Sistem hesabının kendi posta kutusu |
| 2 | ~/imap | Hesaptaki alan adlarının posta kutuları (belgelere göre buradaki bağlantılar meşru da olabilir) |
| 3 | Yedek başlamadan önceki hazırlama alanı (assembly area) | backup_tmpdir ile belirlenen geçici dizin (varsayılan /home/tmp); orada bir şey yoksa ev dizininin tamamı |
| 4 | ~/domains | Web dosyaları: node_modules, sanal ortamlar, snapshot klasörleri |
Sabit bağlantı mı, sembolik bağlantı mı?
ext4, XFS ve benzeri Linux dosya sistemlerinde bir dosyanın üst bilgileri (sahibi, izinleri, boyutu) ve verisinin diskteki yeri inode adı verilen kayıtta tutulur; dizindeki dosya adı yalnızca bu kaydı gösterir. Sabit bağlantı, aynı inode'a verilmiş ikinci bir addır: iki ad eşittir, hiçbiri "asıl dosya" değildir ve son ad silinene kadar veri diskte kalır. ls -l ve stat çıktısındaki bağlantı sayısı (link count), inode'un kaç adı olduğunu gösterir. Sembolik bağlantı (symlink) ise içinde yalnızca bir yol yazan ayrı, küçük bir dosyadır.| Sabit bağlantı | Sembolik bağlantı | |
|---|---|---|
| Nedir? | Aynı inode'un başka bir adı | İçinde yol yazan ayrı bir dosya |
| Sahip ve izinler | Bütün adlarda ortak | Hedef dosyada denetlenir |
| Dosya sistemleri arasında | Mümkün değil | Mümkün |
| tar arşivinde | İçerik bir kez, diğer adlar için bağlantı kaydı | Hedef değil, bağlantının kendisi |
| Bu hatayı tetikler mi? | Evet | Hayır |
DirectAdmin yedeği neden durduruyor?
Bağlantı koruması olmayan bir sistemde kullanıcı, aynı dosya sisteminde bulunan ve sahibi olmadığı bir dosyaya kendi ev dizininde sabit bağlantı oluşturabilir; DirectAdmin belgeleri örnek olarak /etc/shadow dosyasını veriyor. Bağlantı asıl dosyanın sahibini ve izinlerini taşıdığı için kullanıcı dosyayı yine okuyamaz. Tehlike, kullanıcının dizinini yetkili bir sürecin okumasıdır: root yetkisiyle çalışan bir yedekleme, bu dosyayı kullanıcının indirebileceği bir arşive koyardı. DirectAdmin yedek öncesi kontrolü bu nedenle 1.35.1 sürümünde (Şubat 2010) ekledi.Bugün aynı açığı iki katman daha kapatıyor:
- strict_backup_permissions=1 DirectAdmin'in güncel belgelerine göre varsayılan değerdir; arşiv kullanıcı yetkisiyle oluşturulur ve kullanıcı okuyamadığı dosyayı arşive de koyamaz. Seçenek 2010'da eklendiğinde varsayılanı 0'dı; eski bir directadmin.conf dosyasında hâlâ 0 yazıyor olabilir.
- fs.protected_hardlinks=1 olduğunda çekirdek, kullanıcı kaynak dosyanın sahibi değilse ve dosya üzerinde okuma/yazma izni de yoksa sabit bağlantı oluşturmayı reddeder (çekirdek belgeleri). systemd'nin varsayılan sysctl dosyası bu değeri 1 yaptığı için çoğu güncel dağıtımda açıktır; yine de kontrol edin.
1. Adım: Sabit bağlantıları bulun
Komutları root olarak çalıştırın; fred yerine bildirimde adı geçen hesabı yazın.
Bash:
find /home/fred -xdev -type f -links +1 -ls
Code:
1442211 9876 -rwxr-xr-x 2 fred fred 10112345 Oct 20 09:14 /home/fred/domains/example.com/app/node_modules/esbuild/bin/esbuild
1442211 9876 -rwxr-xr-x 2 fred fred 10112345 Oct 20 09:14 /home/fred/domains/example.com/app/node_modules/@esbuild/linux-x64/bin/esbuild
Bash:
# en çok bağlantının bulunduğu dizinler genellikle kaynağı ele verir
find /home/fred -xdev -type f -links +1 -printf '%h\n' | sort | uniq -c | sort -rn | head -20
# bir dosyanın bütün adları (/home ayrı bir bölüm değilse / altında arayın)
find /home -xdev -samefile /home/fred/dosya/yolu
stat -c '%h bağlantı, inode %i, sahip %U: %n' /home/fred/dosya/yolu
# güvenlik kontrolü: fred'in ev dizininde, sahibi fred olmayan bağlantılı dosyalar
find /home/fred -xdev -type f -links +1 ! -user fred -ls
2. Adım: Kaynağı belirleyin
| Yer | Olası neden | Kalıcı çözüm |
|---|---|---|
| ~/imap/…/Maildir, ~/Maildir | Dovecot iletileri sabit bağlantıyla kopyalar (maildir_copy_with_hardlinks varsayılan olarak yes): e-posta istemcisinin iletileri klasörler arasında kopyalaması (IMAP COPY), tek bir iletinin aynı hesaptaki birden çok posta kutusuna LMTP ile teslimi | maildir_copy_with_hardlinks = no, ardından var olan bağlantıları ayırmak |
| node_modules/esbuild/bin/esbuild ve platform paketindeki ikizi | esbuild kurulum betiği, JavaScript başlatıcısını yerel çalıştırılabilir dosyaya açılan bir sabit bağlantıyla değiştirir (Windows dışındaki sistemlerde; Yarn ya da --ignore-scripts kullanılmadığında) | Derlemeyi başka yerde yapıp yalnızca çıktıyı yüklemek ya da her kurulumdan sonra bağlantıyı ayırmak |
| Diğer adı ~/.local/share/pnpm/store ya da ~/.cache/uv altında olan node_modules veya .venv dosyaları | pnpm'in varsayılan içe aktarma yöntemi Linux'ta önce sabit bağlantıyı dener; uv ise ext4 gibi klonlamayı desteklemeyen dosya sistemlerinde klonlamadan sabit bağlantıya geçer | Aracı kopyalama moduna alıp yeniden kurmak |
| Snapshot klasörleri (daily.0, daily.1 … ya da tarih adlı klasörler) | rsync --link-dest ya da cp -al, değişmeyen dosyaları snapshot'lar arasında paylaştırır | Snapshot'ları /home dışındaki bir diskte ya da sunucuda tutmak |
| Başka bir sunucunun arşivinden geri yüklenen e-posta ya da dosyalar | tar, arşivde kayıtlı sabit bağlantıları yeniden oluşturur | Her taşımadan sonra tarayıp bağlantıları ayırmak |
3. Adım: Bu sırayla düzeltin
3.1 Var olan bağlantıları ayırın: önce kopyala, sonra taşı
Aynı üst bilgilere sahip bağımsız bir kopya oluşturun ve bu kopyayı asıl adın üzerine yeniden adlandırın. Aynı dosya sistemi içindeki yeniden adlandırma atomiktir; dosyayı okuyan program yarım yazılmış bir kopya görmez. Diğer ad eski inode'u göstermeye devam eder, inode'un bağlantı sayısı ise bir azalır. Bunun yerine adlardan birini silmeyin: Maildir'de iki ad da kullanıcının farklı klasörlerde gördüğü iletilerdir.Başlamadan önce, ayrılan her bağlantının tam bir kopyaya dönüştüğünü unutmayın. Aşağıdaki komutun verdiği tahmini (bir üst sınırdır) hesabın kotasıyla karşılaştırın; root olarak çalışan bir işlem hesabı kotasının üzerine çıkarabilir ve bu, e-posta teslimini durdurabilir. Hesabın güncel bir kopyasının başka bir yerde bulunduğundan emin olun; posta kutularında önce 3.2'yi uygulayın ve trafiğin az olduğu bir saati seçin.
Bash:
find /home/fred/imap -xdev -type f -links +1 -printf '%s\n' | awk '{s+=$1} END {printf "%.0f MiB\n", s/1048576}'
Bash:
#!/bin/bash
# break-hardlinks.sh DIR [--apply]
set -u
dir="${1:-}"; mode="${2:-}"
[ -d "$dir" ] || { echo "usage: $0 DIR [--apply]" >&2; exit 1; }
find "$dir" -xdev -type f -links +1 -print0 |
while IFS= read -r -d '' f; do
[ -f "$f" ] && [ "$(stat -c %h -- "$f")" -gt 1 ] || continue
if [ "$mode" != "--apply" ]; then echo "would split: $f"; continue; fi
tmp="$(dirname -- "$f")/.split.$$.$(basename -- "$f")"
if cp -p -- "$f" "$tmp" && mv -f -- "$tmp" "$f"; then
echo "split: $f"
else
echo "FAILED: $f" >&2; rm -f -- "$tmp"
fi
done
Bash:
bash break-hardlinks.sh /home/fred/imap # yalnızca listele
bash break-hardlinks.sh /home/fred/imap --apply # ayır
find /home/fred -xdev -type f -links +1 | wc -l # başka kaynak kalmadıysa 0
3.2 Dovecot: e-postayı sabit bağlantıyla kopyalamayı kapatın
maildir_copy_with_hardlinks, Dovecot 2.3'te de 2.4'te de varsayılan olarak yes değerindedir ve belgeler bunu "unlikely to have any side effects", yani yan etkisi pek beklenmeyen bir performans kazancı olarak tanımlar. DirectAdmin'in yedeği için ise bir yan etkisi var. Değişikliği, oluşturulan dovecot.conf dosyasını düzenlemek yerine DirectAdmin belgelerinin diğer Dovecot ayarlarında yaptığı gibi ayrı bir dosyaya yazın:
Bash:
echo 'maildir_copy_with_hardlinks = no' > /etc/dovecot/conf.d/90-maildir-copy.conf
doveconf maildir_copy_with_hardlinks # beklenen: maildir_copy_with_hardlinks = no
systemctl restart dovecot
3.3 Snapshot klasörlerini /home dışına taşıyın
rsync --link-dest ile alınan snapshot'lar, değişmeyen dosyaları paylaştıkları için az yer kaplar. Hesabın içinde bu, binlerce sabit bağlantı ve fazladan kota kullanımı demektir; snapshot'ları ayrı bir diske ya da yedek sunucusuna yazın:
Bash:
bugun=$(date +%F)
mkdir -p /backup/fred
rsync -a --link-dest=/backup/fred/latest /home/fred/domains/ "/backup/fred/$bugun/"
ln -sfn "/backup/fred/$bugun" /backup/fred/latest
3.4 Node.js, Python ve git
- esbuild: Derlemeyi kendi bilgisayarınızda ya da CI'da yapıp sunucuya yalnızca çıktıyı yükleyin. Derleme sunucuda yapılmak zorundaysa her kurulumdan sonra bağlantıyı yukarıdaki betikle ayırın. npm ci --ignore-scripts de bağlantıyı önler ama bütün paketlerin kurulum betiklerini atlar.
- pnpm: pnpm-workspace.yaml ya da genel config.yaml dosyasına packageImportMethod: copy yazın (eski sürümler bu ayarı .npmrc içinde package-import-method=copy olarak okur), ardından node_modules klasörünü silip yeniden kurun.
- uv: pyproject.toml dosyasının tool.uv tablosuna link-mode = "copy" ekleyin ya da UV_LINK_MODE=copy değişkenini tanımlayın; sonra .venv klasörünü silip uv sync çalıştırın.
- git: Yerel bir yoldan klonlama, .git/objects dosyalarını sabit bağlantıyla oluşturur; --no-hardlinks ekleyin.
4. Adım: backup_hard_link_check kararını en sona bırakın
DirectAdmin belgeleri kontrolün "as long as you keep strict_backup_permissions=1", yani strict_backup_permissions=1 korunduğu sürece kapatılabileceğini söylüyor; kontrol yedekler sırasında fazla yük bindiriyorsa ve bütün kullanıcılarınıza güveniyorsanız kapatılabileceğini de belirtiyor. Seçeneği getiren 2010 tarihli sürüm notu ise "It's recommended to leave it on" diyerek açık bırakılmasını öneriyor. Biz bu koşula iki koşul daha ekliyoruz: çekirdeğin bağlantı koruması ve temiz bir sahiplik denetimi. Üçünü de kontrol edin:
Bash:
sysctl fs.protected_hardlinks # 1 olmalı
da config | grep -E 'strict_backup_permissions|backup_hard_link_check'
for d in /home/*/; do u=$(basename "$d"); id -u "$u" >/dev/null 2>&1 || continue
nice -n 19 find "$d" -xdev -type f -links +1 ! -user "$u" -printf "$u: %u %p\n"
done # çıktı olmamalı
Bash:
echo 'fs.protected_hardlinks = 1' > /etc/sysctl.d/90-protected-hardlinks.conf
sysctl --system
Bash:
# A: /usr/local/directadmin/conf/directadmin.conf içinde
# strict_backup_permissions=1
# backup_hard_link_check=0
systemctl restart directadmin
# B: komut satırından
da config-set strict_backup_permissions 1
da config-set backup_hard_link_check 0
systemctl restart directadmin # v1.710'dan beri config-set DirectAdmin'i kendisi yeniden yükler; eski sürümlerde bu satır ya da --restart gerekir
| Durum | Karar |
|---|---|
| Bağlantılar ~/imap altındaydı; Dovecot ayarı değişti, bağlantılar ayrıldı | Kontrol açık kalsın |
| Bağlantılar değiştiremediğiniz bir yazılımdan gelmeye devam ediyor ve üç koşul da sağlanıyor | Kapatmak kabul edilebilir |
| Eski bir yapılandırmada strict_backup_permissions=0 | Kapatmayın; önce 1 yapıp deneme yedeği alın |
| Bir ev dizininde kullanıcının sahibi olmadığı bağlantılı bir dosya var ya da müşterilerinizi tanımıyorsunuz | Açık kalsın; araştırın ve kaynakları düzeltin |
Bash:
chmod 700 /usr/local/sbin/hardlink-audit.sh
echo '30 3 * * 1 root /usr/local/sbin/hardlink-audit.sh' > /etc/cron.d/hardlink-audit
5. Adım: Deneme yedeği, ardından geri yükleme testi
Tamamlanan bir yedek, geri yüklenebilen bir yedek demek değildir. Etkilenen hesabın yedeğini Admin Backup/Transfer (yönetici), Manage User Backups (bayi) ya da Site Backup (kullanıcı) ekranından veya komut satırından oluşturun:
Bash:
/usr/local/directadmin/directadmin admin-backup --destination=/home/admin/admin_backups --user=fred
ls -lh /home/admin/admin_backups/ | grep fred
Bash:
tar -tzf /home/admin/admin_backups/user.admin.fred.tar.gz | less
zstd -dc /home/admin/admin_backups/user.admin.fred.tar.zst | tar -tf - | less # .tar.zst yedekler için
Bash:
doveadm mailbox status -u [email protected] -t messages '*'
Sık yapılan hatalar
- Önce kontrolü kapatıp nedene hiç bakmamak. Başkasına ait bir dosyaya açılmış bağlantının uyarısı da onunla birlikte kaybolur.
- Bağlantıdan kurtulmak için adlardan birini silmek. Maildir'de bu, iletiyi bir klasörden siler.
- -p olmadan kopyalamak. Zaman damgaları, izinler ve root olarak çalışırken sahiplik değişir.
- Geçici kopyayı /tmp altına koymak. Dosya sistemleri arası taşıma atomik bir yeniden adlandırma değildir.
- Büyük bir posta kutusunda kotaya bakmadan bağlantıları ayırmak.
- İzin hatalarını susturmak için strict_backup_permissions=0 yapmak. Yedek bu durumda dosyaları daha yüksek yetkiyle okur; kontrolün var olma nedeni tam da budur.
Sık sorulan sorular
Bu hatanın disk kotasındaki sabit limitle ilgisi var mı?Hayır. Sabit bağlantı (hard link) birden fazla adı olan dosyadır; sabit limit (hard limit) ise kotanın üst sınırıdır. Kota yalnızca bağlantıları ayırırken önem kazanır, çünkü her ayırma tam bir kopya demektir.
Posta kutusundaki sabit bağlantılar tehlikeli mi?
Dosya kullanıcının kendisine aitse hayır; Dovecot'un oluşturduğu bağlantılar böyledir. Kontrol zararsızı kötü niyetliden ayıramadığı için sıradan sunucularda da devreye girer.
maildir_copy_with_hardlinks = no daha fazla disk alanı kullanır mı?
Evet, yeni kopyalarda. Var olan bağlantılar siz ayırana kadar olduğu gibi kalır.
tar, sabit bağlantılı bir dosyayı iki kez saklar mı?
Hayır. GNU tar içeriği bir kez saklar, diğer adlar için bağlantı kaydı ekler ve arşivi açarken bağlantıları yeniden kurar. Yedeğin durması tar'ın bir sınırı değil, DirectAdmin'in güvenlik politikasıdır.
Sembolik bağlantı bu hataya yol açar mı?
Hayır. DirectAdmin belgelerine göre sembolik bağlantılar sorun değildir, çünkü tar hedef dosyayı değil bağlantının kendisini ekler.
Kontrolü kapattık. Şimdi ne yapmalıyız?
strict_backup_permissions=1 ve fs.protected_hardlinks=1 ayarlarını koruyun, haftalık denetimi çalıştırın ve geri yükleme testlerini sürdürün.
Sonuç ve sonraki adımlar
Bu kontrol kaba bir alarmdır: Dovecot'un zararsız kopyalarını /etc/shadow dosyasına açılmış bir bağlantıdan ayıramaz, bu yüzden birinin bakması gerekir. Kaynak sık sık ~/imap altındadır; o durumda tek bir Dovecot ayarı ve bir defalık temizlik, kontrol açık kalırken hataları bitirir.- Bildirimde adı geçen hesap için find komutunu çalıştırın ve kaynağı belirleyin.
- Kaynağı düzeltin: Dovecot ayarı, derleme süreci, snapshot konumu.
- Kalan bağlantıları kopyala-taşı betiğiyle ayırın.
- Kontrolü yalnızca strict_backup_permissions=1, fs.protected_hardlinks=1 ve temiz bir denetim sonucu bir aradaysa kapatın.
- Bir yedek alıp geri yükleyin, ardından geri yükleme testini düzenli olarak tekrarlayın.
DirectAdmin stops a user's backup when it finds a hard link in the data it is about to archive. Every admin gets a notice, that user's archive is not created, and each scheduled run fails the same way until the cause is gone. We handle it on our own servers in the order below. To be clear from the start: this is about hard links (files with more than one name), not about the hard limit of a disk quota.
Short answer
Find, fix, then decide.
- List the files: find /home/USER -xdev -type f -links +1 -ls
- Fix the source: Dovecot copies mail with hard links by default (maildir_copy_with_hardlinks = no stops it), some Node.js and Python tools hard link files from a cache, and snapshots belong outside /home.
- Split the links that already exist: copy each file, then move the copy over the original name.
- Disable the check (backup_hard_link_check=0) only with strict_backup_permissions=1, fs.protected_hardlinks=1 and no links to files the user does not own.
- Finish with a test backup and a test restore.
What the message says
DirectAdmin's current documentation gives the message as:
Code:
Backup Abort (2): A hard link was found under User username's path
| Number | Area (DirectAdmin docs) | Look first at |
|---|---|---|
| 1 | ~/Maildir | The system account's own mailbox |
| 2 | ~/imap | Mailboxes of the account's domains (the docs note that links here can be legitimate) |
| 3 | Backup assembly area, before the backup starts | The staging directory set by backup_tmpdir (default /home/tmp); if that is clean, the whole home directory |
| 4 | ~/domains | Website files: node_modules, virtual environments, snapshots |
Hard link or symbolic link?
On ext4, XFS and similar Linux filesystems, a file's metadata (owner, permissions, size) and the location of its data live in an inode, and a name in a directory just points to it. A hard link is a second name for the same inode: both names are equal, neither is "the original", and the data stays on disk until the last name is deleted. The link count in ls -l and stat output is the number of names. A symbolic link is different: a separate small file that stores a path.| Hard link | Symbolic link | |
|---|---|---|
| What it is | Another name for the same inode | A separate file containing a path |
| Owner and permissions | Shared by all names | Checked on the target |
| Across filesystems | Not possible | Possible |
| In a tar archive | Content once, link entries for the other names | The link itself, not the target |
| Triggers this abort | Yes | No |
Why DirectAdmin aborts
On a system without link protection, a user can hard link a file they do not own into their home directory, provided it sits on the same filesystem; DirectAdmin's documentation uses /etc/shadow as the example. The link keeps the original owner and permissions, so the user still cannot read the file. The danger is a privileged process reading the user's directory: a backup running with root rights would put that file into an archive the user can download. DirectAdmin added the pre-backup check in version 1.35.1 (February 2010) for this reason.Two other layers close the same hole today:
- strict_backup_permissions=1, the default in DirectAdmin's current documentation, builds the archive as the user, who cannot read such a file. Its default was 0 when it was introduced in 2010, so an old directadmin.conf may still say 0.
- fs.protected_hardlinks=1 makes the kernel refuse a hard link unless the user owns the source file or has read/write access to it (kernel documentation). Upstream systemd ships it as 1, so most current distributions have it on. Check anyway.
Step 1: Find the hard links
Run as root and replace fred with the account named in the notice.
Bash:
find /home/fred -xdev -type f -links +1 -ls
Code:
1442211 9876 -rwxr-xr-x 2 fred fred 10112345 Oct 20 09:14 /home/fred/domains/example.com/app/node_modules/esbuild/bin/esbuild
1442211 9876 -rwxr-xr-x 2 fred fred 10112345 Oct 20 09:14 /home/fred/domains/example.com/app/node_modules/@esbuild/linux-x64/bin/esbuild
Bash:
# directories with the most links usually reveal the source
find /home/fred -xdev -type f -links +1 -printf '%h\n' | sort | uniq -c | sort -rn | head -20
# all names of one file (search from / if /home is not a separate mount)
find /home -xdev -samefile /home/fred/path/to/file
stat -c '%h links, inode %i, owner %U: %n' /home/fred/path/to/file
# security check: linked files in fred's home that fred does not own
find /home/fred -xdev -type f -links +1 ! -user fred -ls
Step 2: Identify the source
| Where | Typical cause | Lasting fix |
|---|---|---|
| ~/imap/…/Maildir, ~/Maildir | Dovecot copies messages with hard links (maildir_copy_with_hardlinks is yes by default): a mail client copying messages between folders (IMAP COPY), LMTP delivery of one message to several mailboxes of one account | maildir_copy_with_hardlinks = no, then split the existing links |
| node_modules/esbuild/bin/esbuild and its twin in the platform package | esbuild's install script swaps its JavaScript launcher for a hard link to the native binary (on every OS except Windows, unless Yarn or --ignore-scripts is used) | Build elsewhere and deploy the output, or split the link after each install |
| node_modules or .venv files whose other name is in ~/.local/share/pnpm/store or ~/.cache/uv | pnpm's default import method tries hard links first on Linux; uv falls back from cloning to hard links on filesystems such as ext4 | Switch the tool to copy mode and reinstall |
| Snapshot folders (daily.0, daily.1 … or dated names) | rsync --link-dest or cp -al share unchanged files between snapshots | Keep snapshots on a disk or server outside /home |
| Mail or files restored from another server's archive | tar recreates the hard links stored in the archive | Scan after every migration and split the links |
Step 3: Fix, in this order
3.1 Split the existing links: copy, then move
Make an independent copy with the same metadata, then rename it over the original name. A rename within one filesystem is atomic, so a program reading the file never sees a half-written copy. The other name keeps pointing to the old inode, whose link count drops by one. Do not delete one of the names instead: in a Maildir both are messages the user sees, in different folders.Before you start, remember that every split becomes a full copy. Compare the estimate from the command below (an upper bound) with the account's quota: a process running as root can push the account over its quota, and that can stop its mail delivery. Make sure a recent copy of the account exists elsewhere, and for mailboxes apply 3.2 first and pick a quiet hour.
Bash:
find /home/fred/imap -xdev -type f -links +1 -printf '%s\n' | awk '{s+=$1} END {printf "%.0f MiB\n", s/1048576}'
Bash:
#!/bin/bash
# break-hardlinks.sh DIR [--apply]
set -u
dir="${1:-}"; mode="${2:-}"
[ -d "$dir" ] || { echo "usage: $0 DIR [--apply]" >&2; exit 1; }
find "$dir" -xdev -type f -links +1 -print0 |
while IFS= read -r -d '' f; do
[ -f "$f" ] && [ "$(stat -c %h -- "$f")" -gt 1 ] || continue
if [ "$mode" != "--apply" ]; then echo "would split: $f"; continue; fi
tmp="$(dirname -- "$f")/.split.$$.$(basename -- "$f")"
if cp -p -- "$f" "$tmp" && mv -f -- "$tmp" "$f"; then
echo "split: $f"
else
echo "FAILED: $f" >&2; rm -f -- "$tmp"
fi
done
Bash:
bash break-hardlinks.sh /home/fred/imap # list only
bash break-hardlinks.sh /home/fred/imap --apply # split
find /home/fred -xdev -type f -links +1 | wc -l # expect 0 unless other sources remain
3.2 Dovecot: stop copying mail with hard links
maildir_copy_with_hardlinks defaults to yes in Dovecot 2.3 and 2.4, and the documentation calls it a performance gain "unlikely to have any side effects". For DirectAdmin's backup it does have one. Put the change in its own file, as DirectAdmin's docs do for other Dovecot tweaks, rather than editing the generated dovecot.conf:
Bash:
echo 'maildir_copy_with_hardlinks = no' > /etc/dovecot/conf.d/90-maildir-copy.conf
doveconf maildir_copy_with_hardlinks # expect: maildir_copy_with_hardlinks = no
systemctl restart dovecot
3.3 Snapshots: move them out of /home
Snapshots made with rsync --link-dest are cheap precisely because unchanged files are shared. Inside an account that means thousands of hard links and extra quota use, so write them to a separate disk or a backup server instead:
Bash:
today=$(date +%F)
mkdir -p /backup/fred
rsync -a --link-dest=/backup/fred/latest /home/fred/domains/ "/backup/fred/$today/"
ln -sfn "/backup/fred/$today" /backup/fred/latest
3.4 Node.js, Python and git
- esbuild: build on a workstation or in CI and upload only the output. If you must build on the server, split the link after each install with the script above. npm ci --ignore-scripts also avoids it, but skips every package's install scripts.
- pnpm: set packageImportMethod: copy in pnpm-workspace.yaml or the global config.yaml (older versions read package-import-method=copy from .npmrc), then delete node_modules and reinstall.
- uv: set link-mode = "copy" in the tool.uv table of pyproject.toml, or export UV_LINK_MODE=copy, then delete .venv and run uv sync.
- git: cloning from a local path hard links .git/objects; add --no-hardlinks.
Step 4: Only then decide about backup_hard_link_check
DirectAdmin's documentation allows disabling the check "as long as you keep strict_backup_permissions=1", and mentions doing so when the check adds too much load during backups and you trust all your users. The 2010 release note that introduced the option adds: "It's recommended to leave it on." We add two conditions of our own: the kernel's link protection and a clean ownership audit. Check all three:
Bash:
sysctl fs.protected_hardlinks # want 1
da config | grep -E 'strict_backup_permissions|backup_hard_link_check'
for d in /home/*/; do u=$(basename "$d"); id -u "$u" >/dev/null 2>&1 || continue
nice -n 19 find "$d" -xdev -type f -links +1 ! -user "$u" -printf "$u: %u %p\n"
done # want no output
Bash:
echo 'fs.protected_hardlinks = 1' > /etc/sysctl.d/90-protected-hardlinks.conf
sysctl --system
Bash:
# A: in /usr/local/directadmin/conf/directadmin.conf
# strict_backup_permissions=1
# backup_hard_link_check=0
systemctl restart directadmin
# B: from the command line
da config-set strict_backup_permissions 1
da config-set backup_hard_link_check 0
systemctl restart directadmin # since v1.710 config-set reloads DirectAdmin by itself; older versions need this or --restart
| Situation | Decision |
|---|---|
| Links were in ~/imap; Dovecot is changed and the links are split | Keep the check on |
| Links keep coming from software you cannot change, and all three conditions hold | Disabling is acceptable |
| strict_backup_permissions=0 in an old configuration | Do not disable; set it to 1 and test a backup first |
| A linked file in a home that its user does not own, or customers you do not know | Keep it on; investigate and fix the sources |
Bash:
chmod 700 /usr/local/sbin/hardlink-audit.sh
echo '30 3 * * 1 root /usr/local/sbin/hardlink-audit.sh' > /etc/cron.d/hardlink-audit
Step 5: Test backup, then test restore
A backup that finishes is not yet a backup that restores. Create one for the affected account in Admin Backup/Transfer (admin level), Manage User Backups (reseller) or Site Backup (user), or from the shell:
Bash:
/usr/local/directadmin/directadmin admin-backup --destination=/home/admin/admin_backups --user=fred
ls -lh /home/admin/admin_backups/ | grep fred
Bash:
tar -tzf /home/admin/admin_backups/user.admin.fred.tar.gz | less
zstd -dc /home/admin/admin_backups/user.admin.fred.tar.zst | tar -tf - | less # for .tar.zst backups
Bash:
doveadm mailbox status -u [email protected] -t messages '*'
Common mistakes
- Disabling the check first and never looking for the cause. The warning about a link to someone else's file disappears with it.
- Deleting one name to get rid of a link. In a Maildir that removes the message from a folder.
- Copying without -p, which changes timestamps, permissions and, as root, ownership.
- Putting the temporary copy in /tmp. A move across filesystems is not an atomic rename.
- Splitting links in a large mailbox without checking the quota.
- Setting strict_backup_permissions=0 to silence permission errors. The backup then reads files with more privileges, the very case the check exists for.
FAQ
Is this related to disk quota limits?No. A hard link is a file with more than one name; a hard limit is a quota ceiling. Quota matters only when you split links, because each split is a full copy.
Are hard links in a mailbox dangerous?
Not when the file belongs to the user, which is the case with Dovecot's links. The check cannot tell harmless from malicious, so it fires on ordinary servers.
Will maildir_copy_with_hardlinks = no use more disk space?
Yes, for new copies. Existing links stay until you split them.
Does tar store a hard-linked file twice?
No. GNU tar stores the content once, adds link entries for the other names and recreates the links on extraction. The abort is DirectAdmin's policy, not a tar limitation.
Can a symbolic link cause this abort?
No. DirectAdmin's documentation says symbolic links are not an issue, because tar adds the link itself, not the file it points to.
We disabled the check. What now?
Keep strict_backup_permissions=1 and fs.protected_hardlinks=1, run the weekly audit and keep testing restores.
Conclusion and next steps
The check is a blunt alarm: it cannot tell Dovecot's harmless copies from a link to /etc/shadow, so someone has to look. The source is often in ~/imap, and then one Dovecot setting plus a one-time cleanup ends the aborts with the check still on.- Run the find command for the account in the notice and identify the source.
- Fix the source: Dovecot setting, build process, snapshot location.
- Split the remaining links with the copy-then-move script.
- Disable the check only with strict_backup_permissions=1, fs.protected_hardlinks=1 and a clean audit.
- Test a backup and a restore, then repeat restore tests on a schedule.
Last edited: