File Permissions in Linux: chmod, chown, and umask
1. Overview
“Permission denied” is one of the most common errors when deploying applications to Linux. Applications can’t write logs, can’t read config files, or secret files become readable by every user.
This guide explains Linux’s file permission model and how to use three main tools: chmod to change permissions, chown to change ownership, and umask to control default permissions for new files. By the end, you’ll be able to read any file’s permissions, grant the minimum necessary access, and manipulate POSIX permissions from Java.
2. Reading file permissions with ls -l
Start with an example directory:
mkdir -p shop && cd shop
echo "id,total" > order.csv
mkdir reports
ls -l
-rw-r--r-- 1 an dev 9 Oct 13 09:00 order.csv
drwxr-xr-x 2 an dev 4096 Oct 13 09:00 reports
The first column has 10 characters:
- Character 1 is the type:
-is a regular file,dis a directory,lis a symbolic link. - The remaining 9 characters are divided into three groups of 3 characters each: user (owner), group, other (everyone else).
- Each group consists of
r(read),w(write),x(execute). A dash-means no permission.
The third and fourth columns are the owner (an) and group (dev). So with order.csv, an can read and write, group dev members can only read, and other users can only read.
To see permissions in numeric form, use stat:
stat -c '%a %A %U:%G %n' order.csv reports
644 -rw-r--r-- an:dev order.csv
755 drwxr-xr-x an:dev reports
The kernel checks permissions in order. If you’re the owner, only the user permission group applies. If you’re not the owner but belong to the group, only the group permission group applies. Otherwise, the other permissions apply. The groups are not cumulative. Therefore, a file with permissions ---r--r-- will prevent even the owner from reading it, even though everyone else can read it.
3. Changing permissions with chmod
3.1. Symbolic form
Symbolic form has the syntax [ugoa][+-=][rwxX]. Where u is user, g is group, o is other, a is all.
chmod g+w order.csv # add write permission for group
chmod o-r order.csv # remove read permission from others
chmod u=rw,go= order.csv # user: rw, group and others: nothing
stat -c '%a %A' order.csv
600 -rw-------
The + and - operators only add or remove the specific permissions mentioned; other permissions remain unchanged. The = operator reassigns the entire group. Symbolic form is safe when you only want to modify part of the permissions without knowing the current state.
3.2. Octal form
Each permission has a value: r = 4, w = 2, x = 1. Adding the values gives one digit for each group.
| Number | Permission | Used for |
|---|---|---|
| 7 | rwx |
owner of a directory or script |
| 6 | rw- |
owner of a data file |
| 5 | r-x |
read and enter directory, run script |
| 4 | r-- |
read-only |
| 0 | --- |
no permissions |
For example, create a script and a password file:
printf '#!/bin/bash\necho "Backup orders..."\n' > backup.sh
echo "db.password=s3cret" > secret.properties
chmod 755 backup.sh
chmod 600 secret.properties
./backup.sh
stat -c '%a %n' backup.sh secret.properties
Backup orders...
755 backup.sh
600 secret.properties
755 allows everyone to run the script but only the owner can modify it. 600 allows only the owner to read and write the secret file. Numeric form assigns all permissions at once, so it’s suitable when you know exactly the desired state.
3.3. Recursive permission changes and capital X
A classic mistake is running the following command:
chmod -R 755 shop/
This command assigns execute permission to every file, including order.csv. The correct way is to use capital X. This symbol only adds x to directories, or to files that already have x in some group:
chmod -R u=rwX,go=rX shop/
The result is directories have 755, regular files have 644, and backup.sh keeps 755. For more precise control, you can separate using find:
find shop -type d -exec chmod 750 {} +
find shop -type f -exec chmod 640 {} +
4. Directory permissions differ from file permissions
With directories, rwx has different meanings:
r: list names of items inside (ls).w: create, delete, rename items inside. This permission requiresxto take effect.x: enter the directory (cd) and access items inside if you know their name.
A surprising consequence: deleting a file depends on the directory’s permissions, not the file’s permissions.
mkdir inbox && echo "data" > inbox/invoice.txt
chmod 444 inbox/invoice.txt # read-only file
rm -f inbox/invoice.txt # can still delete it because inbox has w permission
ls inbox # empty
Conversely, if you remove x permission from the directory, you can’t read the files inside even if they have 644 permissions:
echo "data" > inbox/invoice.txt
chmod 644 inbox/invoice.txt
chmod u-x inbox
cat inbox/invoice.txt
cat: inbox/invoice.txt: Permission denied
When an application reports Permission denied but the file’s permissions look correct, check all parent directories in the path. The namei -l /path/to/file command lists permissions at each level. Before proceeding, restore the directory’s permission with chmod u+x inbox.
5. Changing ownership with chown and chgrp
chown changes the owner, group, or both:
sudo chown appuser order.csv # change only the owner
sudo chown appuser:dev order.csv # change both owner and group
sudo chown :dev order.csv # change only the group
sudo chown -R appuser:appuser /opt/shop
Some important points to remember:
- Only
rootcan change the owner. Regular users cannot “give away” their files to someone else. - Regular users can change the group of files they own, but only to a group they belong to. The command is
chgrp dev order.csv. chown -Rrecursively follows the directory tree. When it encounters a symbolic link, by default it changes the owner of the file the link points to. Add-hif you want to change the owner of the link itself.
A common pattern when deploying is to run the service with its own user, while the operations team shares a common group:
sudo chown -R appuser:ops /var/log/shop
sudo chmod -R u=rwX,g=rX,o= /var/log/shop
With this setup, the service can write logs, the ops group can read logs, and other users cannot access them.
6. Default permissions with umask
When creating a new file, programs typically request 666 for files and 777 for directories. The kernel then masks off bits present in umask. The result is:
actual permissions = requested permissions AND (NOT umask)
umask # view current value
umask 027 # change for current shell
touch customer.csv && mkdir archive
stat -c '%a %n' customer.csv archive
0022
640 customer.csv
750 archive
With umask 027: files get 666 minus 027 bits, resulting in 640, and directories get 777 minus 027, resulting in 750. Others lose all permissions, group loses write permission.
Note:
umaskis a bitwise mask operation, not arithmetic subtraction. Withumask 033, new files have permission644, not633. The reason is that bitxis not in666to begin with, so there’s nothing to mask off.
Some common values:
| umask | File | Directory | When to use |
|---|---|---|---|
022 |
644 | 755 | default on most distros |
027 |
640 | 750 | servers, share only within group |
077 |
600 | 700 | sensitive data |
The umask command only affects the current process and its children. To set it permanently for a systemd service, declare it in the unit file:
[Service]
User=appuser
UMask=0027
7. Three special bits: setuid, setgid, sticky
Beyond the 9 basic bits, Linux has three special bits. They’re written as a fourth digit at the start of octal form:
- setuid (4): program runs with the owner’s permissions. For example,
/usr/bin/passwdshows as-rwsr-xr-x. - setgid (2): when set on a directory, new files created inside inherit the directory’s group. This bit is very useful for shared work directories.
- sticky (1): when set on a directory, only the file’s owner can delete it.
/tmpuses this bit and shows asdrwxrwxrwt.
For example, creating a shared directory for the dev group:
sudo mkdir /srv/shared && sudo chown :dev /srv/shared
sudo chmod 2770 /srv/shared
stat -c '%a %A' /srv/shared
2770 drwxrws---
Note: Don’t set setuid on scripts or files you wrote unless you fully understand the risks. A buggy setuid root program is a classic privilege escalation path.
8. Manipulating POSIX permissions from Java
Java NIO supports POSIX permissions via PosixFilePermissions. The class below writes a secret file and ensures only the owner can read it:
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermissions;
public class SecretWriter {
public Path writeSecret(Path dir, String content) throws IOException {
Path file = dir.resolve("secret.properties");
Files.writeString(file, content);
Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-------"));
return file;
}
}
fromString("rw-------") converts the symbolic string to Set<PosixFilePermission>. Then setPosixFilePermissions works like the chmod 600 command.
You might try passing PosixFilePermissions.asFileAttribute(...) to Files.createFile. This still applies umask, just like the open() system call. For sensitive files, call setPosixFilePermissions after creation to ensure the result. An even safer approach is creating the file with strict permissions from the start, to avoid a window where other users can read the file.
Testing with JUnit 5 and AssertJ:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermissions;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
class SecretWriterTest {
@TempDir
Path tempDir;
@Test
void givenSecret_whenWritten_thenOnlyOwnerCanReadAndWrite() throws Exception {
assumeTrue(FileSystems.getDefault().supportedFileAttributeViews().contains("posix"));
Path file = new SecretWriter().writeSecret(tempDir, "db.password=s3cret");
String perms = PosixFilePermissions.toString(Files.getPosixFilePermissions(file));
assertThat(perms).isEqualTo("rw-------");
assertThat(Files.readString(file)).contains("db.password");
}
}
assumeTrue will skip the test on Windows, where POSIX attribute view is not available. Without this line, the test would throw UnsupportedOperationException.
9. Common mistakes
- Using
chmod 777for a “quick fix”. This opens write permission to every user on the machine. Determine which user the process runs as (ps -o user= -p <pid>), then grant permissions only to that user or group. - Running
chmod -R 755on a directory tree. This makes every file executable. Use capitalXorfindas shown in section 3.3. - Only checking the file’s permissions. You need to check parent directories too with
namei -l. - Running
sudothen forgetting to change ownership. Files created bysudobelong toroot, so applications run by regular users can’t write to them. Runchownimmediately after. - Mounting volumes in Docker. UIDs inside the container may differ from the host. The kernel checks permissions by UID number, not user name.
- SSH rejects keys. SSH ignores
~/.ssh/authorized_keysor private keys if permissions are too permissive. Directory~/.sshneeds permission700, private keys need permission600.
10. Conclusion
Linux’s permission model consists of three groups (user, group, other) and three permissions (r, w, x). These permissions have different meanings for files and directories.
You use chmod to change permissions: symbolic form when you only need to modify part of them, numeric form when you want to assign them all. You use chown and chgrp to assign files to the right user and group. You use umask so new files are created with safe permissions from the start.
The overarching principle is to grant the minimum necessary permissions. Run services with dedicated users, share via groups, and keep others at 0 for anything sensitive.