You ran rsync. It finished. You felt briefly powerful. Then every service started failing because half your files are now owned by the wrong user,
have the wrong mode, or lost their ACLs. The new Debian 13 box looks fine until you try to start PostgreSQL, reload nginx, or let your app write to its own cache.
Permission chaos after rsync is rarely “rsync being weird.” It’s almost always us giving it the wrong instructions, copying across a boundary where
identity changes (UID/GID mapping), or copying onto a filesystem that can’t represent what you’re trying to preserve. This piece is the clean way to preserve
ownership on Debian 13—what to do, what to avoid, and how to prove you got it right.
The mental model: what “ownership” actually is
On Debian (and Linux generally), a file does not belong to a “username.” It belongs to a numeric user ID and group ID. Names are just labels in
/etc/passwd and /etc/group (or LDAP/SSSD), translated at display time. If you copy a file owned by UID 1001 from Host A to Host B,
and UID 1001 is “alice” on Host A but “postgres” on Host B, you just assigned that file to “postgres” on Host B. rsync did what you asked; you asked the wrong
question.
Ownership preservation has three layers:
- Mode bits (rwx for user/group/other plus setuid/setgid/sticky): stored per inode; most filesystems support them.
- Owner/group numeric IDs (uid/gid): stored per inode; whether you can set them depends on privilege and mount options.
- Extended permissions (POSIX ACLs) and metadata (xattrs like SELinux labels, capabilities, user.* tags): optional and filesystem-dependent.
rsync can preserve all of this, but only if you tell it to and if the destination can represent it. If you aim rsync at a destination that lies (hello, some CIFS
configurations), or you run it without the privileges needed to set ownership, you’ll get “success” and broken permissions. That’s not a paradox; it’s a Tuesday.
Fast diagnosis playbook
When permissions “look wrong” after rsync, the fastest path is to determine which layer broke: IDs, bits, or extras. Don’t guess. Measure.
1) First: confirm what’s wrong (IDs vs modes vs ACL/xattr)
- If ownership is wrong across the board: suspect UID/GID mismatch, privilege, or root-squash.
- If modes are wrong (e.g., everything 755): suspect umask, missing
-p/-a, or a destination that normalizes permissions. - If only specific apps fail (e.g., systemd units, ssh keys, binaries): suspect missing xattrs/capabilities, missing setuid bits, or lost ACLs.
2) Second: check the transport boundary
- Local disk to local disk: simplest; rsync should preserve nearly everything as root.
- Over SSH: depends on remote rsync and privileges; root-to-root works if configured; otherwise you’ll see silent downgrades.
- NFS: root-squash will happily wreck “preserve ownership” without telling you in a way your tired brain will notice.
- CIFS/SMB: permission semantics may be emulated; preservation might be impossible or mapped.
3) Third: reproduce on a single file with maximal verbosity
Pick one file with known owner/mode/ACL/xattr, rsync it with -vv, and inspect it on both ends. You’re looking for the exact class of metadata that
failed to transfer.
Paraphrased idea from Gene Kim (DevOps operations author): “Reliability comes from making work visible and repeatable, not from heroics at 2 a.m.”
Facts and historical context (why this keeps happening)
- UID/GID are the source of truth. Usernames are lookup sugar; rsync can preserve IDs even if the name doesn’t exist—if you ask it to.
- rsync’s “archive mode” is a bundle.
-ais shorthand for several flags, but not all the ones people assume. - POSIX ACLs predate many “modern” Linux conveniences. They’ve been in enterprise Unix for decades and made their way into Linux as first-class citizens, but they’re still often forgotten during migrations.
- Linux capabilities are stored in xattrs. If you lose xattrs, a binary with
cap_net_bind_servicemight stop binding to port 80 without being setuid root. - NFS root-squash is intentionally hostile to “preserve ownership.” It’s a security feature that maps root to nobody, commonly breaking naive backup/restore schemes.
- Hard links are not “copied” unless you preserve them. Without
-H, rsync can duplicate linked files, inflating disk usage and changing semantics for some package-managed trees. - Sticky bit and setgid directories matter for shared write paths. Copying mode bits wrong in places like
/tmpor shared group dirs can become a security issue, not just an outage. - SELinux labels and AppArmor metadata live outside classic mode bits. Even on Debian (often AppArmor-first), xattrs may matter for container runtimes, snap-like environments, or custom LSM setups.
- Some filesystems can’t represent the metadata you want. FAT and exFAT are the obvious ones, but CIFS mounts can be “Unixy” or “not Unixy” depending on options.
What rsync preserves (and what it doesn’t)
The first lie we tell ourselves is: “I used -a, so everything is the same.” Archive mode is excellent, but it’s not “every metadata bit in the
universe.” It’s:
-rrecurse-lpreserve symlinks-ppreserve permissions (mode bits)-tpreserve times-gpreserve group-opreserve owner (requires root)-Dpreserve devices and specials (requires root)
What -a does not automatically include:
- ACLs unless you add
-A - xattrs unless you add
-X - hard links unless you add
-H(and yes, this can be expensive) - numeric ID preservation across differing name services unless you add
--numeric-ids
That’s the core: if you care about ownership, you care about numeric identity. If you care about “app still works,” you care about ACLs, xattrs, and sometimes
capabilities.
Joke #1: rsync doesn’t “lose permissions.” It follows instructions with the confidence of an intern who never asks clarifying questions.
The clean command lines for Debian 13 migrations
Here are opinionated defaults that behave well in real migrations. Adjust deliberately. If you can’t explain why you removed a flag, keep it.
Local-to-local copy (same machine, different disks)
If you’re copying within one host (or into a mounted filesystem where you have real root privileges), this is the clean baseline:
cr0x@server:~$ sudo rsync -aHAX --numeric-ids --info=stats2,progress2 /src/ /dst/
...output...
Why this set:
-a: preserve core metadata-H: preserve hard links (useful for some system trees, container layers, package caches)-A: preserve ACLs-X: preserve xattrs (capabilities, labels, custom metadata)--numeric-ids: preserve UID/GID numbers, not names (critical across hosts)
Over SSH, root to root (preferred for full-fidelity migrations)
cr0x@server:~$ sudo rsync -aHAX --numeric-ids --info=stats2,progress2 -e "ssh -o BatchMode=yes" /src/ root@newhost:/dst/
...output...
If root SSH is disallowed (common and not inherently bad), use an automation account plus sudo on the destination and run rsync in server mode. That gets nuanced.
For most teams, the better choice is: copy as root over SSH from an allowed management network, then close the door again.
When you can’t be root (and you still want sane results)
If you’re unprivileged, you cannot set arbitrary owners. rsync will preserve what it can. Don’t pretend you’re doing a faithful migration; treat it like a data
copy where ownership must be reassigned after.
cr0x@server:~$ rsync -aAX --info=stats2,progress2 /src/ user@newhost:/dst/
...output...
Note: -o and -g inside -a won’t succeed in the way you want without privilege. rsync will still run. It will just not
perform miracles.
When the destination is NFS (handle root-squash like an adult)
If NFS export uses root-squash (often default), root on the client cannot set ownership. You have three clean options:
- Temporarily export with
no_root_squashfrom a restricted host for the migration window (security reviewed, time-boxed). - Migrate to local disk on the destination server, then move data server-side on the NFS server (preserving ownership locally).
- Accept that you cannot preserve ownership and plan a controlled chown/chgrp/ACL reapply.
Practical tasks: commands, outputs, decisions
You don’t fix permissions by squinting at ls -l and hoping. You fix permissions by asking the system questions and letting the answers force your
next step. Here are practical tasks I actually run during migrations and incident response.
Task 1: Confirm rsync version and compiled features (ACL/xattr support)
cr0x@server:~$ rsync --version
rsync version 3.2.7 protocol version 32
Capabilities:
64-bit files, 64-bit inums, 64-bit timestamps, ACLs, xattrs, iconv, symtimes
...output...
What it means: You’re looking for ACLs and xattrs in capabilities. If they’re missing, flags like -A
and -X won’t do what you think.
Decision: If ACLs/xattrs aren’t supported, upgrade rsync or change method (tar with xattrs, filesystem snapshot replication, etc.).
Task 2: Inspect a known-problem file’s full metadata (mode, UID/GID, ACL)
cr0x@server:~$ sudo stat -c '%n mode=%a uid=%u(%U) gid=%g(%G)' /dst/app/config.yml
/dst/app/config.yml mode=640 uid=1001(alice) gid=1001(alice)
What it means: Numeric IDs are the truth; names in parentheses are local interpretation. If the UID is “right” but the name is “wrong,” you have
a mapping problem, not a copy problem.
Decision: If UID/GID numbers differ from source expectations, stop and fix identity mapping before copying more.
Task 3: Check ACLs exist and compare expected access
cr0x@server:~$ sudo getfacl -p /dst/app
# file: /dst/app
# owner: 1001
# group: 1001
user::rwx
group::r-x
group:33:r-x
mask::r-x
other::---
What it means: The directory grants read/execute to group 33 (often www-data). If this ACL vanished, your web service can’t read
the app tree even though the mode bits “look okay.”
Decision: If ACLs are missing where you expect them, you must re-run rsync with -A and ensure the destination filesystem supports
ACLs and they’re enabled.
Task 4: Check xattrs on a file that might carry capabilities or labels
cr0x@server:~$ sudo getfattr -d -m- /dst/usr/local/bin/myhelper
# file: /dst/usr/local/bin/myhelper
security.capability=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA=
What it means: The binary has Linux capabilities stored in an xattr. Lose that, and you may get mysterious “permission denied” on privileged
operations without any ownership changes.
Decision: If xattrs are missing, re-copy with -X. If the destination filesystem can’t store xattrs, you need a different target.
Task 5: Validate that rsync is actually running as root on both ends
cr0x@server:~$ sudo rsync -a --rsync-path="id && rsync" /src/ root@newhost:/dst/ 2>/dev/null | head
uid=0(root) gid=0(root) groups=0(root)
What it means: The remote side printed its identity before running rsync. If you see a non-root UID, ownership preservation will be limited.
Decision: If remote isn’t root, fix SSH/sudo approach or accept post-copy chown strategy.
Task 6: Detect UID/GID mismatch between hosts for a specific service account
cr0x@server:~$ getent passwd www-data
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
cr0x@server:~$ ssh root@newhost getent passwd www-data
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
What it means: Good: consistent UID/GID for www-data. But don’t stop here; your app users are usually custom accounts, not Debian
defaults.
Decision: If IDs don’t match, either align them (preferred before copy) or use --numeric-ids and fix name service after.
Task 7: Inventory “unknown” numeric owners on the destination
cr0x@server:~$ sudo find /dst -xdev -printf '%u %g %p\n' | awk '$1 ~ /^[0-9]+$/ || $2 ~ /^[0-9]+$/' | head
1001 1001 /dst/app
1001 1001 /dst/app/config.yml
What it means: Numeric owners with no name resolution. That’s not inherently wrong: it might be correct numeric preservation with missing account
definitions.
Decision: If many numeric IDs appear, fix /etc/passwd//etc/group consistency (or LDAP/SSSD) before letting services
run.
Task 8: Confirm mount options allow POSIX permissions and ACLs
cr0x@server:~$ mount | grep " on /dst "
/dev/sdb1 on /dst type ext4 (rw,relatime,acl)
What it means: ext4 mounted with acl support. On some setups ACLs are default-enabled; on others, explicit mount option matters.
Decision: If you don’t see acl and ACLs aren’t behaving, remount with ACL enabled or adjust filesystem defaults.
Task 9: Detect root-squash on an NFS mount the hard way (by trying)
cr0x@server:~$ sudo touch /dst/.rootsquash-test && sudo chown 0:0 /dst/.rootsquash-test; ls -ln /dst/.rootsquash-test
-rw-r--r-- 1 65534 65534 0 Dec 29 12:00 /dst/.rootsquash-test
What it means: UID/GID became 65534 (nobody/nogroup). That’s classic root-squash behavior.
Decision: Stop trying to preserve ownership over this mount. Change export policy temporarily, or copy server-side.
Task 10: Dry-run rsync with itemized changes to see what metadata would change
cr0x@server:~$ sudo rsync -aHAX --numeric-ids -n -i /src/ /dst/ | head
.d..t...... ./
>f..tpogax app/config.yml
>f..t...... app/static/logo.png
What it means: Itemized output encodes changes. In >f..tpogax, letters show what will be updated: time (t),
perms (p), owner (o), group (g), ACL (a), xattr (x).
Decision: If you see lots of o/g changes you didn’t expect, your ownership is not aligned; fix before executing.
Task 11: Confirm a directory tree has setgid/sticky bits where expected
cr0x@server:~$ sudo find /dst/shared -maxdepth 1 -type d -printf '%m %p\n'
2775 /dst/shared
2775 /dst/shared/team-a
2770 /dst/shared/private
What it means: 2 in the leading digit indicates setgid on directories (group inheritance). If these flipped to 0775,
new files may land in the wrong group, causing slow-burning access bugs.
Decision: If setgid/sticky bits are missing, re-copy with permissions preserved, or explicitly fix them and re-audit.
Task 12: Verify that “special files” (devices, sockets) weren’t mangled
cr0x@server:~$ sudo find /dst/dev -maxdepth 1 -type c -o -type b | head
/dst/dev/null
/dst/dev/zero
/dst/dev/random
What it means: Device nodes exist. If you copied a system image or chroot and these became regular files, something went very wrong (likely
missing root privileges or omitting -D).
Decision: For system trees, always copy as root with -a. If devices are wrong, rebuild them (or recopy properly) before booting
or chrooting.
Task 13: Check for “everything owned by the rsync user” symptom
cr0x@server:~$ sudo find /dst -xdev -printf '%u\n' | sort | uniq -c | sort -nr | head
152340 backup
2193 root
812 www-data
What it means: Most files are owned by backup. That usually means the copy ran unprivileged, or the destination refused chown.
Decision: If you need fidelity, redo the copy with proper privilege and a destination that allows ownership changes. Don’t try to “fix it later”
unless you have a deterministic mapping.
Task 14: Compare source and destination ownership for a sample quickly
cr0x@server:~$ sudo (cd /src && find . -maxdepth 2 -type f -printf '%u:%g %m %p\n' | head) > /tmp/src.sample
cr0x@server:~$ sudo (cd /dst && find . -maxdepth 2 -type f -printf '%u:%g %m %p\n' | head) > /tmp/dst.sample
cr0x@server:~$ diff -u /tmp/src.sample /tmp/dst.sample
--- /tmp/src.sample
+++ /tmp/dst.sample
@@
-alice:alice 640 ./app/config.yml
+postgres:postgres 640 ./app/config.yml
What it means: Same mode, different ownership. The file is readable, but by the wrong identity. This is either a UID mismatch or name-based mapping.
Decision: Use --numeric-ids (or align accounts) and redo, or commit to a planned ownership rewrite.
UID/GID mismatch: the silent permissions killer
Debian 13 didn’t invent UID/GID mismatch, but it’s a common moment for it because upgrades/migrations often coincide with directory services changes, container
adoption, or “we’ll clean up users later.” Later is how you get outages.
Typical failure mode: on the old host, your app user is UID 1005. On the new host, UID 1005 belongs to a different service account created earlier. You rsync
“preserving ownership.” Now your app’s secrets belong to some unrelated daemon. The app won’t start, and you’ve also created an accidental privilege leak.
What to do instead (opinionated and boring)
- Before copying, ensure the destination has the same UID/GID assignments for service accounts that own data.
- Use
--numeric-idsso rsync doesn’t try to be clever about name mapping. - If you rely on LDAP/SSSD, validate it’s up on the destination before you start the copy. A temporary fallback to local users can create mismatches.
When you can’t align IDs
Sometimes you inherit a mess: the new environment uses centralized identity and the old one was hand-crafted. In that case, do not attempt “preserve ownership”
as a religion. Treat it like a controlled transformation:
- Copy data with integrity (content and timestamps) first.
- Then apply ownership/ACL policy deterministically (scripts, manifests, or app-level tooling).
- Finally run a permission audit and start services under supervision.
ACLs and xattrs: preserving the “invisible” permissions
Mode bits are the first 80% of permissions. ACLs and xattrs are the last 20% that cause 80% of outages.
ACLs: the “why can’t www-data read it?” trap
ACLs are commonly used to grant a service group read access to directories owned by a different group, or to allow developers to deploy without making the whole
tree group-writable. If you lose them, ls -l will not confess. You might see a + in some outputs, but people ignore it the way they
ignore check engine lights.
Preserve ACLs with -A. Then verify with getfacl on a few known paths. If you’re migrating application servers, pay special attention
to:
- shared upload directories
- deployment directories with mixed ownership
- paths where a service user needs access but shouldn’t own files
xattrs: capabilities, labels, and the “but it worked yesterday” effect
Extended attributes are used for everything from SELinux contexts to user-defined metadata to capabilities. On Debian, capabilities are the big one in
production: they’re a clean way to allow a binary to bind low ports or do raw networking without running as root.
Preserve xattrs with -X. Then check a known binary or file that you expect to carry xattrs. If xattrs aren’t preserved, symptoms can be strange:
services fail to bind, containers fail to start, or security tooling behaves differently.
Filesystem boundaries: ext4 vs XFS vs ZFS vs NFS vs CIFS
rsync can only preserve what the destination filesystem can store, and what the mount options allow you to set. The easiest way to get “permissions chaos” is to
copy from a Unix filesystem to something that only pretends to be one.
Local Linux filesystems (ext4, XFS)
Generally safe for mode bits, ownership, ACLs, xattrs. Watch mount options. Also watch if you’re copying into a filesystem with different default ACL behavior.
ZFS on Linux
ZFS supports permissions, ACLs, xattrs, but ACL mode can differ (POSIX vs NFSv4 ACL). If you see odd ACL translation, confirm dataset properties and how your
tooling expects ACLs to look. rsync can preserve ACLs, but if the underlying ACL model differs, “preserved” may not mean “equivalent.”
NFS
NFS is fantastic until you want to do Unix ownership operations from a client as root and the server says “absolutely not.” That’s root-squash. It’s normal.
Plan for it instead of fighting it.
CIFS/SMB
CIFS mounts can map ownership and permissions based on mount options and server capabilities. Some setups can store Unix mode bits; others map everything to a
single UID/GID. If your destination is CIFS and you need real Unix semantics, test with a small set of files before migrating anything serious.
Joke #2: CIFS permissions are like office coffee machines—someone swears it’s configured correctly, and then it makes mud.
Three corporate mini-stories from the trenches
Mini-story 1: The incident caused by a wrong assumption
A team migrated a fleet of Debian application servers to new hardware. The plan was clean: build Debian 13, rsync the app trees, flip the load balancer.
They used rsync -a and a non-root “deploy” account because root SSH was banned (reasonable policy). The migration completed early. Everyone went to
lunch with that uneasy feeling you ignore when the graph is green.
The first error reports were about “random 403s” and “uploads failing.” They rolled one server, same. Rolled back to old pool, everything fine. Back to new,
broken. People argued about network paths and firewall rules because that’s what people do when they don’t want to think about file permissions.
The actual issue: ACLs were in use on the old servers to grant the web worker group access to a directory tree owned by a different service account. The deploy
user could read the files, so rsync copied the contents fine, but it did not preserve the ACLs because -A wasn’t used. Mode bits looked similar.
The missing ACLs only mattered when the web service tried to write new objects.
Fixing it wasn’t heroic. They re-ran rsync with -aA (and then -X for completeness) using a controlled sudo workflow on the
destination. Then they added an ACL verification check into the migration runbook: pick three known ACL-heavy directories, compare getfacl output,
fail the migration if mismatched.
The wrong assumption was simple: “archive mode means everything.” Archive mode means “a lot,” not “all.”
Mini-story 2: The optimization that backfired
Another organization had a large media pipeline moving data between servers nightly. They wanted to speed up a slow rsync window, so they removed “expensive”
flags and turned on aggressive options. Someone proposed: drop -H and -X, and add --inplace and --no-owner
because “ownership doesn’t matter for media files.” It benchmarked faster on a quiet lab dataset. The change went live.
Two weeks later, disk usage on the destination grew unexpectedly. It wasn’t a neat “we need more storage” problem; the jobs started failing mid-run due to full
volumes, which cascaded into downstream queue backlogs. Operations spent a night deleting temporary directories and replaying jobs. The postmortem originally
blamed “unexpected input growth.” That was comforting and wrong.
The root cause was hard links. Some of the source trees used hard linking to de-duplicate identical payloads generated by earlier stages. Removing -H
meant rsync copied each hard-linked file as a separate file, multiplying space usage. The ownership flags change also removed a subtle safety check: a few
directories were protected by group ownership rules and setgid; flattening ownership made later jobs write where they shouldn’t, producing confusing “success” with
wrong output placement.
They rolled back the “optimization,” then reworked the pipeline properly: keep -H where the dataset uses it, keep -X for capability-bearing tools,
and focus performance work on I/O patterns (batching, avoiding unnecessary rescans) instead of ripping out correctness flags. Fast wrong is still wrong; it just
fails with confidence.
Mini-story 3: The boring but correct practice that saved the day
A third team did a Debian 13 migration for a legacy monolith. The app was old enough to have opinions about file modes, and the deployment tree had a mixture of
setgid directories, a few weird ACL grants, and one binary with capabilities so it could bind a low port without root. The team’s lead was stubborn about process:
preflight checks, dry-runs, and a small canary copy before touching the main dataset.
They started with identity: ensured service accounts had matching UID/GID across old and new servers. Not “same name,” same numbers. They verified LDAP/SSSD
availability. Then they ran rsync dry-run with itemized changes and captured output in a ticket. It was tedious. It was also the point.
During the canary, they noticed a handful of files would lose xattrs. The destination was an NFS mount with root-squash. Nobody had mentioned it because it was
“just storage.” That detail would have broken the entire cutover: binaries would lose capabilities, and some directories would be owned by nobody. Because they
caught it early, they changed the plan: stage data to local disk first, then do a server-side move within the storage host where ownership could be preserved.
Cutover day was dull. Services came up. Monitoring stayed quiet. The win wasn’t genius; it was refusing to skip the boring checks that catch permission problems
before customers do.
Common mistakes: symptom → root cause → fix
-
Symptom: Everything on destination is owned by the user who ran rsync.
Root cause: rsync ran unprivileged, or destination refusedchown(root-squash, CIFS mapping).
Fix: Run rsync as root end-to-end on a filesystem that supports ownership; or accept a policy-driven chown/ACL reapply after copy. -
Symptom: Owners look right by name, but services still fail with “permission denied.”
Root cause: Lost ACLs or xattrs; mode bits alone aren’t enough.
Fix: Re-run with-Aand-X; verify withgetfaclandgetfattr. -
Symptom: Files show correct modes, but ownership is assigned to the wrong local account.
Root cause: UID/GID mismatch across hosts; rsync mapped by name without preserving numeric IDs, or destination resolves names differently.
Fix: Align UID/GID before migration; use--numeric-ids; validate withstatandgetent. -
Symptom: Web app can read files but cannot write uploads or caches after migration.
Root cause: Missing setgid/sticky bits or missing directory default ACLs that grant write access for the service group.
Fix: Preserve permissions and ACLs; reapply default ACLs; validate withgetfaclon directories. -
Symptom: A binary that used to bind to port 80 now fails unless run as root.
Root cause: Lost capability xattr (security.capability).
Fix: Re-copy with-X, or reapply capabilities usingsetcapand then ensure future migrations preserve xattrs. -
Symptom: Disk usage exploded after migration; “identical” trees consume far more space.
Root cause: Hard links not preserved because-Homitted.
Fix: Re-run with-Hfor datasets that use hard links; consider whether the cost is acceptable and test on a subset. -
Symptom: rsync reports success, but special files/devices are wrong (or missing).
Root cause: Copy wasn’t run as root or didn’t preserve specials (-Dvia-a), or destination filesystem can’t store them.
Fix: Use root + archive mode onto a compatible filesystem; for system images, consider snapshot-based migration instead. -
Symptom: Some directories are writable by “other” after migration.
Root cause: Someone used--chmodbroadly or copied through a mount that normalizes permissions; or fixed one symptom by loosening everything.
Fix: Undo broad chmod. Restore from source with proper preservation flags. Audit withfindfor world-writable paths.
Checklists / step-by-step plan
Plan A: Full-fidelity migration (preferred)
- Confirm identity alignment: service accounts and groups have consistent UID/GID across source and destination. If you use LDAP/SSSD, verify it works on the destination.
- Validate destination filesystem semantics: local Linux filesystem preferred. Avoid CIFS for Unix-permission-critical trees. Treat NFS root-squash as a hard constraint.
- Pick a canary directory: include a file with ACLs and a binary with xattrs/capabilities if applicable.
-
Dry-run rsync with
-n -iand save itemized output. Look for owner/group/ACL/xattr changes. -
Real rsync:
sudo rsync -aHAX --numeric-ids --info=stats2,progress2. -
Verify: sample
stat,getfacl,getfattr. Run a small app smoke test that touches write paths. - Final sync: re-run rsync to catch deltas right before cutover (same flags).
- Cutover: start services; monitor logs for permission-related errors; keep old data read-only until confidence is high.
Plan B: Controlled transformation (when IDs cannot match)
- Copy content with timestamps: use rsync but do not pretend ownership will match.
- Apply ownership policy: explicit
chown/chgrpand ACL scripts per directory. - Validate with an audit: check for numeric owners, unexpected world-writable files, and missing expected ACLs.
- Document the mapping so the next migration isn’t archaeology.
Plan C: NFS destination with root-squash (pragmatic choices)
- Prefer staging to local disk on the destination host, preserving ownership there.
- Then move data server-side (on the NFS server) into the exported path, where it can preserve ownership locally.
- If you must copy directly to NFS: accept that ownership preservation may be impossible and treat it as a re-ownership job.
FAQ
1) Does rsync -a preserve ownership?
It includes -o and -g, but it only truly preserves ownership if rsync is privileged enough to set uid/gid on the destination. Over SSH,
that typically means root on the remote side too.
2) Why do I need --numeric-ids?
Because names can map to different numeric IDs on different hosts. --numeric-ids tells rsync to preserve the numbers exactly, avoiding “helpful”
name mapping that can silently reassign ownership.
3) I ran as root, but ownership is still wrong. How?
Common causes: destination is NFS with root-squash; destination is CIFS with forced uid/gid mapping; or you copied into a filesystem/mount that refuses chown.
Test with a manual chown on the destination path.
4) What flags preserve ACLs and xattrs?
-A preserves ACLs. -X preserves extended attributes. If you care about “apps still run,” you usually want both.
5) Should I always use -H?
Not always. -H can increase memory usage and runtime, because rsync has to track hard link relationships. Use it when your dataset uses hard links
(some system trees, deduplicated content, certain package caches) and you care about preserving them.
6) Can I preserve ownership when copying as a non-root user?
Not in the general case. A non-root user can only set ownership to itself (and sometimes groups it belongs to). If you need faithful ownership, use root or a
controlled privileged workflow.
7) Why do permissions look fine but systemd services fail?
Services can fail due to missing capabilities on binaries (xattrs), missing access to runtime dirs due to ACL loss, or wrong ownership on state directories under
/var. Check getfattr for capabilities and getfacl for directories.
8) Is it safe to “fix everything” with chmod -R 777 or broad chown -R?
It’s fast, it “works,” and it’s how you turn a migration into a security incident. If you need to rewrite ownership, do it with a mapped plan and audit the
result.
9) What’s the quickest way to prove source and destination match?
Use rsync dry-run with itemized changes (-n -i) and focus on p, o, g, a, and x
markers. Then validate a sample with stat, getfacl, and getfattr.
10) I’m copying to a filesystem that doesn’t support Unix ownership. What now?
Then you can’t “preserve ownership” in any meaningful way. Either change the destination (preferred), or accept that permissions will be represented differently
and enforce access through that system’s model instead of pretending it’s POSIX.
Conclusion: next steps you can actually execute
If you want Debian 13 migrations to be boring, stop treating permissions as a side effect. Treat them as primary data. Ownership is numeric IDs, not names. ACLs
and xattrs are not optional when you’re moving real production systems. And filesystem boundaries are where good intentions go to die.
Practical next steps:
- Pick one problem directory and run the canary checks:
stat,getfacl,getfattr. - Run an rsync dry-run with
-aHAX --numeric-ids -n -iand read the letters like they’re a contract. - Confirm UID/GID alignment for your service accounts with
getenton both hosts. - If you’re targeting NFS/CIFS, test ownership setting with a single file before starting the migration. If it can’t chown, it can’t preserve ownership—arguing won’t help.
- Then do the real copy with the full-fidelity flag set, and verify before cutover.