📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

How do file permissions work on Linux and Windows? Your .NET app works locally but throws UnauthorizedAccessException when writing to a mounted folder in a container. How do you fix it properly?

Unix permission bits, owners and groups, Windows ACLs, how .NET exposes them, and fixing permission errors for non-root containers with mounted volumes.

Mid-Level Debugging Intermediate acl containers file-system linux permissions security windows

🎯 Key points

  • Linux checks owner, then group, then others rwx bits (octal 0644, 0755, 0600); root bypasses most checks.
  • Creating, deleting or renaming needs w and x on the directory, not on the file itself.
  • Windows uses ACLs: a DACL of allow and deny ACEs, usually inherited from the parent; deny wins, and the read-only attribute is not a security control.
  • The kernel compares numeric UIDs and GIDs only, and bind mounts keep the host's numbers, so names in the container can be misleading (use ls -ln and id).
  • The official .NET 10 container images (since .NET 8) define a non-root user app with UID 1654 exposed as $APP_UID; running as it is best practice.
  • Fix ownership declaratively: chown at build time, group write plus Kubernetes fsGroup, or uid/gid/file_mode mount options for SMB or NFS volumes that ignore fsGroup.
  • .NET exposes Unix modes through File.GetUnixFileMode, File.SetUnixFileMode, Directory.CreateDirectory(path, mode) and FileStreamOptions.UnixCreateMode, and Windows ACLs through FileSystemAclExtensions.
  • Permission failures cascade into application failures (500s on upload, broken caches, failed log writes), so add a startup writable-folder check that fails fast with a clear message.

⏱ 30-second answer

  • Linux: each file has an owner (UID), a group (GID) and 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.
  • Windows: each file has an ACL (a list of allow and deny entries for users and groups), usually inherited from the parent folder.

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.

🗣 Full answer (senior)

Linux permissions

$ 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.

Windows ACLs

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.

Production example

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.

🔍 What the interviewer is checking

Checks practical Linux and Windows permission knowledge and security judgement when the quick fix is to make everything writable.

Green flags

  • Explains rwx for owner/group/others and directory semantics.
  • Knows Windows uses ACLs and inheritance.
  • Diagnoses UID mismatch with id, ls -ln, namei -l.
  • Fixes with fsGroup or ownership, keeps non-root.

Red flags

  • chmod 777 or USER root as the fix.
  • Does not know x on a directory means traverse.

📚 Learn in depth

1. Octal notation and umask

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).

2. How the kernel decides

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 ◄┘

3. .NET APIs

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)

4. Containers

  • Official .NET 8+ images define a non-root user: USER $APP_UID (UID 1654). Running as non-root limits damage from a compromise.
  • Bind mounts keep the host's UID/GID numbers, which may not match any user in the container.
  • Kubernetes options: runAsUser, runAsGroup, fsGroup (volume group ownership), readOnlyRootFilesystem: true with an emptyDir for temp files.
  • Some volume types (NFS, Azure Files SMB) ignore fsGroup; ownership is set by mount options (uid=, gid=, dir_mode=, file_mode=).

5. Windows hosting identities

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

6. Try it yourself

  1. 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.
  2. sudo chown 1654 data (or chgrp + chmod g+w) and rerun.
  3. In C#, call File.SetUnixFileMode("secret.key", UnixFileMode.UserRead | UnixFileMode.UserWrite) and check with ls -l.

💻 Code

// 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: {} }

⚠ Common mistakes

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.

🏛 Senior insight: trade-offs

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.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate confuses access denied with file in use Why: Separates permission errors from locking errors
    How to answer

    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.

    • Windows enforces share modes (mandatory). A conflicting open fails with a sharing violation IOException (Win32 error 32).
    • Linux has no share modes. .NET emulates 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.

  2. When: Candidate mentions limiting what the app can read Why: Connects permissions to path traversal defence
    How to answer

    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.

🛠 Real-world problems: how you handle them

Uploads fail after moving to a non-root .NET container
Situation
[Document upload service] on .NET [10] in [AKS] with a persistent volume at `/data/uploads`. Security asked us to run as non-root.
Signals
After deployment, every upload returned 500 with `UnauthorizedAccessException`; reads of existing files still worked.
Investigation
`kubectl exec` then `id` showed uid=1654; `ls -ln /data` showed the folder owned by 0:0 with mode 0755. `namei -l` confirmed no write bit for others.
Resolution
Added `fsGroup: 1654` to the pod security context, redeployed, and confirmed new files were created with group 1654. Kept `runAsNonRoot` and `readOnlyRootFilesystem`.
Prevention
Startup writable-folder probe, a Helm chart default for `fsGroup`, and a deployment checklist item for volume ownership.

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.

↩ Usually asked after

More in File System

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me