Unix permission bits, owners and groups, Windows ACLs, how .NET exposes them, and fixing permission errors for non-root containers with mounted volumes.
rwx bits for owner, group and others (for example 0644, 0755). On a directory, x means "can enter" and w means "can create, delete or rename entries". Root bypasses most checks.The container error usually means the app runs as a non-root user (the official .NET 10 images define user app, UID 1654, via $APP_UID) while the mounted folder is owned by root with 0755. Fix it by giving that UID or a shared group write access (chown, Kubernetes fsGroup, or volume mount options), not by running the container as root or using chmod 777.
$ ls -l /data
-rw-r----- 1 app app 812 Oct 23 10:00 config.json
drwxr-x--- 2 root root 4096 Oct 23 10:00 uploads
│└┬┘└┬┘└┬┘ │ │
│ │ │ │ owner group
│ │ │ └─ others: r-x / ---
│ │ └──── group: r-- / r-x
│ └─────── owner: rw- / rwx
└───────── type: - file, d directory, l symlink
| Bit | On a file | On a directory |
|---|---|---|
| r (4) | Read content | List names |
| w (2) | Modify content | Create, delete, rename entries |
| x (1) | Execute | Enter / traverse |
The kernel checks owner bits if you are the owner, else group bits if you are in the group, else others. Deleting a file needs w on the directory, not on the file.
Each file or folder has a security descriptor with an owner and a DACL: an ordered list of Access Control Entries such as "Users: Read & Execute" or "IIS AppPool\Orders: Modify". Entries usually inherit from the parent. Deny entries win over allow. Tools: Explorer → Security tab, icacls, and in .NET FileSystemAclExtensions.GetAccessControl.
We moved an upload service to the official .NET 10 image and switched to the non-root app user. Uploads failed with UnauthorizedAccessException: Access to the path '/data/uploads/x.pdf' is denied. kubectl exec and ls -ln /data showed the persistent volume was owned by UID 0 with drwxr-xr-x. Our process ran as UID 1654, so it could read but not create files.
Fix: added securityContext.fsGroup: 1654 to the pod so Kubernetes made the volume group-writable by that GID, and kept runAsNonRoot: true. We also removed a previous "fix" someone had tried, chmod 777, from a debug branch.
Checks practical Linux and Windows permission knowledge and security judgement when the quick fix is to make everything writable.
Green flags
id, ls -ln, namei -l.fsGroup or ownership, keeps non-root.Red flags
chmod 777 or USER root as the fix.x on a directory means traverse.rwx r-x r-x = 4+2+1, 4+0+1, 4+0+1 = 0755 (typical directory / executable)
rw- r-- r-- = 0644 (typical file)
rw- --- --- = 0600 (secrets, private keys)
New files get requested mode & ~umask. With the common umask 022, a file requested as 0666 becomes 0644.
Special bits: setuid/setgid (run as file owner/group), sticky on /tmp (only owners can delete their files).
process (uid=1654, gids=[1654]) wants to create /data/uploads/x.pdf
│
▼
/data/uploads owner=0 group=0 mode=0755
│
uid==owner? no ──► in group 0? no ──► others bits = r-x ──► no 'w' ──► EACCES
│
.NET throws UnauthorizedAccessException ◄┘
| Need | API |
|---|---|
| Read/set Unix mode (.NET 7+) | File.GetUnixFileMode, File.SetUnixFileMode, UnixFileMode enum |
| Create with a mode | Directory.CreateDirectory(path, UnixFileMode.UserRead \| UserWrite \| UserExecute), FileStreamOptions.UnixCreateMode |
| Windows ACLs | FileSystemAclExtensions.GetAccessControl/SetAccessControl with FileSecurity, FileSystemAccessRule |
| Simple attributes | File.SetAttributes(path, FileAttributes.ReadOnly) (not a security boundary) |
USER $APP_UID (UID 1654). Running as non-root limits damage from a compromise.runAsUser, runAsGroup, fsGroup (volume group ownership), readOnlyRootFilesystem: true with an emptyDir for temp files.fsGroup; ownership is set by mount options (uid=, gid=, dir_mode=, file_mode=).| Host | Identity that needs access |
|---|---|
| IIS | IIS AppPool\<PoolName> |
| Windows Service | The service account (prefer a gMSA or virtual account NT SERVICE\<name>) |
| Scheduled task | The task's run-as account |
docker run --rm -it -v $PWD/data:/data --user 1654 mcr.microsoft.com/dotnet/runtime:10.0 sh -c "touch /data/x": see Permission denied.sudo chown 1654 data (or chgrp + chmod g+w) and rerun.File.SetUnixFileMode("secret.key", UnixFileMode.UserRead | UnixFileMode.UserWrite) and check with ls -l.// Create a private directory and file with explicit Unix permissions (.NET 7+)
if (!OperatingSystem.IsWindows())
{
Directory.CreateDirectory("/data/keys",
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); // 0700
}
var options = new FileStreamOptions
{
Mode = FileMode.CreateNew,
Access = FileAccess.Write,
UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite // 0600
};
await using (var fs = new FileStream("/data/keys/signing.key", options))
{
await fs.WriteAsync(keyBytes);
}
// Fail fast at startup with a clear message instead of failing on the first upload
static void EnsureWritable(string dir)
{
var probe = Path.Join(dir, $".write-test-{Guid.NewGuid():N}");
try { File.WriteAllText(probe, "ok"); File.Delete(probe); }
catch (UnauthorizedAccessException ex)
{
throw new InvalidOperationException(
$"Storage folder '{dir}' is not writable by UID {Environment.GetEnvironmentVariable("APP_UID") ?? "?"}.", ex);
}
}
# Kubernetes: keep non-root, give the volume to the app's group
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1654
runAsGroup: 1654
fsGroup: 1654
containers:
- name: uploads
image: myregistry/uploads:1.4.0
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
volumeMounts:
- { name: data, mountPath: /data }
- { name: tmp, mountPath: /tmp }
volumes:
- { name: data, persistentVolumeClaim: { claimName: uploads-pvc } }
- { name: tmp, emptyDir: {} }
Mistake: Fixing permission errors with chmod 777 or by running the container as root.
Why it is wrong: It works, but any compromise of the app can now write anywhere, and other users or containers sharing the volume can tamper with your files.
Instead: Grant the minimum: correct owner, or group plus g+w, Kubernetes fsGroup, or volume mount options.
Mistake: Checking only the file's permissions when a create or delete fails.
Why it is wrong: Creating, deleting and renaming need w and x on the parent directory.
Instead: Inspect the parent with ls -ld and the whole path with namei -l /data/uploads/x.pdf.
Mistake: Assuming user names match between the host and the container.
Why it is wrong: The kernel only checks numbers, so UID 1000 on the host may be a different user, or no user, inside the container.
Instead: Use ls -ln and id, and align the numbers with the container's $APP_UID.
Mistake: Expecting fsGroup to work on every volume type.
Why it is wrong: SMB-based volumes such as Azure Files ignore it, because ownership is set when the share is mounted.
Instead: Set uid, gid, dir_mode and file_mode in the mount options or StorageClass.
Mistake: Leaving secrets readable by everyone.
Why it is wrong: Files created under the common umask 022 end up 0644.
Instead: Create key files with UnixCreateMode set to 0600, or mount secrets from a secret store with restrictive modes.
Permission errors in containers are almost always an identity mismatch between the process UID or GID and the storage owner, not a code bug. The senior move is to keep least privilege and fix ownership at the platform level, so the fix is declarative and repeatable across environments.
| Fix | Security | Repeatable | When to use it |
|---|---|---|---|
chown in the Dockerfile for image-owned folders |
Good | Yes | Folders inside the image |
Kubernetes fsGroup plus group write |
Good | Yes | Block volumes (Azure Disk, EBS, local PV) |
Mount options (uid, gid, file_mode) |
Good | Yes | SMB or NFS shares (Azure Files) |
Init container that runs chown |
Acceptable | Yes | Volumes where fsGroup is too slow on huge trees |
chmod 777 |
Poor | Sort of | Never in production |
| Run as root | Poor | Yes | Never for app containers |
I pair this with runAsNonRoot, readOnlyRootFilesystem, allowPrivilegeEscalation: false and an emptyDir for temp files, plus a startup probe that checks writable folders. On Windows the same thinking applies with ACLs and dedicated identities (gMSA, virtual service accounts, IIS app pool identities) rather than LocalSystem or an admin account.
When you open a file you choose an access (Read, Write) and a share mode (FileShare.None, Read, ReadWrite, Delete) saying what others may do while you hold it.
IOException (Win32 error 32).FileShare.None with an advisory flock, which only other cooperating processes respect.Debug by finding the holder (Process Explorer / handle.exe on Windows, lsof / fuser on Linux). Fix by opening with the right FileShare, disposing handles promptly, retrying transient locks (antivirus, indexers) with backoff, and coordinating writers explicitly instead of relying on lock errors.
It is a path traversal vulnerability. file=../../../etc/passwd resolves outside the reports folder, and file=/etc/passwd is worse: Path.Combine discards the base path when the second argument is rooted. The endpoint also has no authorisation and leaks whether files exist.
Fix: do not let users name files. Map an ID to a stored path (database), or if names are required, take only Path.GetFileName(file), build the path, Path.GetFullPath it and check it starts with the root plus a separator, using the platform's case rules. Serve through PhysicalFileProvider or Results.File with the checked path, and add authorisation.
How to tell it (STAR)
**S:** After we switched our [upload service] to the non-root .NET image, uploads failed for [all users]. **T:** I had to fix it without undoing the security change. **A:** In the pod, `id` showed UID 1654 and the volume was owned by root with 0755, so we could read but not create files. I added `fsGroup: 1654` instead of running as root and added a startup check that fails fast if storage is not writable. **R:** Uploads recovered in [20 minutes], we stayed non-root, and the probe has since caught [two] misconfigured environments before users noticed.
Last updated 23 Oct 2026.