Best Practices 4 min read by syncopio Team

Why Your Secure Folders Became 0755 After Migration

Directory permissions are easy to overlook during migration. If your tool creates directories before copying files, those directories probably have default permissions, not the ones you set.

You migrated your file server. Sizes match. Checksums pass. You sign off on the project.

Three weeks later, someone notices that /srv/confidential, which was mode 0700 on the old server, is now 0755 on the new one. Any local user can read it. The files inside have the right permissions. The folder itself doesn’t.

This is more common than you’d think, and almost nobody checks for it.

Why directories lose their permissions

Most migration tools, scripts, and even hand-rolled cp commands follow the same pattern:

  1. Create the destination directory
  2. Copy files into it
  3. Maybe set file metadata afterward

Step 1 is the problem. When a tool creates a directory, it needs some mode. The near-universal default is 0755. The directory is a container, a thing that needs to exist before files can go into it. Permission preservation is an afterthought, if it’s a thought at all.

Here’s what that looks like in practice:

# What most scripts do internally
mkdir -p /dest/srv/confidential    # created as 0755
cp source_files... /dest/srv/confidential/
# done. Never looked at the source directory's mode.
# What actually existed on the source
drwx------  root root  /srv/confidential     # 0700
drwxr-x---  root staff /srv/finance           # 0750
drwxrwx---  root dev   /srv/shared-builds     # 0770

After migration, every single one of those is 0755. World-readable.

Security impact

A directory with permissions 0755 means every user on the system can list its contents and read files inside it (assuming file permissions allow). If the source had 0700, that access was intentionally restricted. Your migration just opened it up.

How to check right now

Compare directory permissions between source and destination with a single find command:

# On the source
find /srv -type d -exec stat -c '%a %n' {} \; | sort > /tmp/source-dirs.txt

# On the destination
find /dest/srv -type d -exec stat -c '%a %n' {} \; | sort > /tmp/dest-dirs.txt

# Compare (strip the path prefix first)
diff <(sed 's| /srv| |' /tmp/source-dirs.txt) \
     <(sed 's| /dest/srv| |' /tmp/dest-dirs.txt)

If every line in the diff shows 0755 on the right side, your tool didn’t preserve directory permissions.

For a quick spot check on a single directory:

stat -c '%a' /source/path/to/directory
stat -c '%a' /dest/path/to/directory

If those two numbers don’t match, you have a problem.

The parent directory trap

This one is subtle. Even tools that do preserve permissions on “known” directories often miss intermediate parents.

Say your source has this structure:

/data/projects/client-a/reports/2025/

Your tool sees /data/projects/client-a/reports/2025/quarterly.pdf in the file list. It needs the full directory tree to exist before it can write that file. So it does the equivalent of:

os.MkdirAll("/dest/data/projects/client-a/reports/2025", 0755)
mkdir -p /dest/data/projects/client-a/reports/2025

Every directory in that chain gets created with 0755. Even if the tool later comes back to set permissions on /dest/data/projects/client-a/, the intermediate directories (/data, /data/projects) may never get corrected because they weren’t in the scan results as explicit entries.

mkdir -p always uses defaults

Both Go’s os.MkdirAll and shell mkdir -p create intermediate directories with the default mode (masked by umask). They don’t accept per-level modes. Any tool that uses these functions to create parent directories on the fly will produce 0755 parents regardless of what the source had.

What rsync does right (and what it misses)

rsync with the -a flag does handle this, mostly:

rsync -a /source/ /dest/

The -a flag includes -p (preserve permissions), which applies to both files and directories. After the transfer, rsync makes a second pass to set directory permissions. This is necessary because directories must be writable during the copy, then locked down afterward.

Where rsync can still miss:

  • Interrupted transfers. If rsync dies mid-run, directories created so far keep their temporary 0755 mode. The permission-fixup pass never ran.
  • Exclude filters. If you exclude certain files but not the directory itself, rsync may create the directory without ever processing its metadata.
  • Partial syncs. Running rsync with --ignore-existing or --update may skip the directory metadata pass for directories it considers “already done.”
  • NFS mount umask. Some NFS server configurations enforce a umask that silently clamps permissions. rsync reports success, but the server stored something different.

Verify after rsync too

Even with rsync -a, run the find ... stat comparison above. Don’t assume. It takes 30 seconds and can save you from a permissions audit failure weeks later.

The three things that need preserving

Directory permissions are just one piece. A complete migration preserves three attributes for every directory:

AttributeWhat it controlsCommon default
Mode (permissions)Who can read, write, enter0755
Ownership (uid/gid)Which user and group owns itroot:root
Timestamps (mtime)When it was last modifiedTime of migration

Most tools that miss permissions also miss timestamps. Ownership is preserved slightly more often because tools running as root tend to use chown for files and directories alike. But timestamps on directories are frequently ignored because they’re considered cosmetic.

They’re not cosmetic if you have monitoring or compliance tools that track “last modified” dates on directory structures.

syncopio advantage

syncopio preserves permissions, ownership, and timestamps on every directory, including intermediate parent directories created during transfer. It applies directory metadata after all files are written, so restrictive permissions don’t block the copy. No second pass, no manual fixup.

Post-migration permissions checklist

After any migration, regardless of the tool you used:

1. Compare directory modes

find /source -type d -exec stat -c '%a %n' {} \; | sort > /tmp/src.txt
find /dest -type d -exec stat -c '%a %n' {} \; | sort > /tmp/dst.txt
diff /tmp/src.txt /tmp/dst.txt

2. Check for 0755 everywhere

# This finds directories that are 0755 on dest but NOT 0755 on source
# (i.e., something changed)
find /dest -type d -perm 0755 -exec stat -c '%n' {} \;

If the list is suspiciously long, and your source had varied permissions, the tool used defaults.

3. Check ownership

find /source -type d -exec stat -c '%U:%G %n' {} \; | sort > /tmp/src-own.txt
find /dest -type d -exec stat -c '%U:%G %n' {} \; | sort > /tmp/dst-own.txt
diff /tmp/src-own.txt /tmp/dst-own.txt

4. Check restrictive directories specifically

These are the ones that matter most:

# Find directories that SHOULD be restrictive (not world-readable)
find /source -type d ! -perm -o=r -exec stat -c '%a %n' {} \;
# Now check if the destination matches

5. Spot check timestamps

stat -c '%y %n' /source/some/directory
stat -c '%y %n' /dest/some/directory

If the destination timestamp is the date of your migration instead of the original date, timestamps weren’t preserved.

Fix it after the fact

If you’ve already migrated and need to repair permissions:

# Generate a fixup script from the source
find /source -type d -exec stat -c 'chmod %a "%n"' {} \; \
  | sed 's|/source|/dest|' > fixup-perms.sh

# Review it first
less fixup-perms.sh

# Apply
bash fixup-perms.sh

For ownership:

find /source -type d -exec stat -c 'chown %U:%G "%n"' {} \; \
  | sed 's|/source|/dest|' > fixup-owners.sh

bash fixup-owners.sh

This works, but it’s a band-aid. The right solution is using a tool that preserves this metadata during the initial transfer.


Further reading:

Ready to simplify your migrations?

See how syncopio can save you hours on every migration project.

Request a Demo