Permission Denied Errors in Go File Operations Fixed
Fix permission denied in Go: file and directory modes, the execute bit on scripts and directories, container users, privileged ports and detection.

Permission denied is the operating system doing its job. A Go program asked for something the user it runs as is not allowed to do, and the kernel said no. The error is reliable and the cause is usually mundane: a file with the wrong mode, a directory missing its execute bit, a container user that does not own a volume. What makes it annoying is that the Go error names the path and not the missing permission, so you have to work out which of several checks failed.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
This page lists the causes with a reproduction of the ones that work anywhere, and the commands to find the others. It sits next to no such file or directory and belongs to the error handling guide. Everything ran on Go 1.25.6 on macOS as a non-root user.
How to detect it
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
dir, _ := os.MkdirTemp("", "demo")
defer os.RemoveAll(dir)
os.Chdir(dir)
os.WriteFile("secret.txt", []byte("x"), 0o000)
_, err := os.ReadFile("secret.txt")
var pe *fs.PathError
if errors.As(err, &pe) {
fmt.Println(pe.Op, "->", pe.Err)
}
fmt.Println("is permission error:", errors.Is(err, fs.ErrPermission))
info, _ := os.Stat("secret.txt")
fmt.Println("mode:", info.Mode())
}open -> permission denied
is permission error: true
mode: ----------The file was created with mode 0o000, no permissions for anyone, and opening it fails with permission denied. errors.Is(err, fs.ErrPermission) is the portable test, working through wrapping, and os.Stat shows the mode that explains it: ----------. (If you run this as root, the operating system lets root read it anyway and the error does not appear. That fact is itself a common source of "works on my machine, fails in production".)
Cause 1: the file mode
A Unix file has an owner, a group and a mode with three triplets of read, write and execute bits for owner, group and everyone else. The kernel picks the triplet that matches the calling process: its user is the owner, else its group matches, else "other". A file with mode 0o640 is readable by its owner and group and by nobody else, so a process running as a different user gets permission denied.
Creating files in your own code needs the right bits at creation time. os.WriteFile(path, data, 0o600) makes a file only the owner can read and write, which is right for secrets and wrong for a file another service needs to read. The permission argument is also masked by the process's umask, which removes bits, so asking for 0o666 often yields 0o644. If another process cannot read what you wrote, ls -l shows the mode and owner, and the fix is to create it with the mode that process needs or to change it with os.Chmod afterwards.
Cause 2: a directory without the execute bit
This one catches people because the file looks fine. To reach a file you need execute permission on every directory in its path, and to list or create entries in a directory you need read or write on it as well:
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
dir, _ := os.MkdirTemp("", "demo")
defer os.RemoveAll(dir)
os.Chdir(dir)
os.Mkdir("vault", 0o755)
os.WriteFile("vault/notes.txt", []byte("hello"), 0o644)
os.Chmod("vault", 0o600) // read and write, but no execute (search) bit
_, err := os.ReadFile("vault/notes.txt")
fmt.Println("file is 0644, but:", errors.Is(err, fs.ErrPermission))
os.Chmod("vault", 0o755) // restore so cleanup can proceed
_, err = os.ReadFile("vault/notes.txt")
fmt.Println("after restoring the directory bit:", err)
}file is 0644, but: true
after restoring the directory bit: <nil>The file itself is world-readable, and the read still fails, because the process cannot traverse vault. When a path fails and the file's mode looks right, check each directory on the way, from the root down. namei -l /full/path on Linux prints the mode and owner of every component in one command, which finds this at once. Directories also need the write bit to create or remove files inside them, so os.Create in a read-only directory fails with the same message.
Cause 3: executing a file without the execute bit
Starting a subprocess from a file that lacks +x fails with permission denied from fork/exec, which reads like a file-reading problem but is about running:
package main
import (
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
)
func main() {
dir, _ := os.MkdirTemp("", "demo")
defer os.RemoveAll(dir)
os.Chdir(dir)
os.WriteFile("hello.sh", []byte("#!/bin/sh\necho hi\n"), 0o644)
err := exec.Command("./hello.sh").Run()
fmt.Println("0644:", errors.Is(err, fs.ErrPermission))
os.Chmod("hello.sh", 0o755)
out, err := exec.Command("./hello.sh").Output()
fmt.Printf("0755: %q %v\n", out, err)
}0644: true
0755: "hi\n" <nil>The script runs once it has the execute bit. The usual real-world versions are a deployment that copied the binary without preserving its mode, an archive or a Docker COPY from a file system that loses permissions, and a volume mounted with noexec. Fix it with chmod +x at build or deploy time, or set the mode explicitly in the Dockerfile with COPY --chmod=755.
Cause 4: the process runs as someone else
Services rarely run as the user who developed them. A systemd unit sets User=, a container image sets USER, a Kubernetes pod sets runAsUser, and each of those changes whose files the process can touch. The common cases are:
- A container running as a non-root user that cannot write to a directory created by root during the image build. Create and
chownthe directory in the Dockerfile before theUSERline. - A mounted volume owned by a different uid on the host than the container's user. Align the uids, or set
fsGroupin Kubernetes so the volume is group-writable. - A config or key file readable by root only, read by a service running as
app. Give the group access and add the service to it, instead of loosening the mode for everyone. - SELinux or AppArmor denying access although the mode bits allow it. The denial shows in the audit log, and
ls -Zshows the context. The Go error is the same.
To find out who the process is, print os.Getuid() and os.Getgid() at start-up, or run id inside the container. Compare with ls -ln on the path.
Cause 5: privileged ports
On Linux, binding a port below 1024 needs root or the CAP_NET_BIND_SERVICE capability. A server that tries :80 or :443 as a normal user fails with listen tcp :80: bind: permission denied. The fixes, in order of preference: listen on a high port such as 8080 and let a proxy or load balancer own 80 and 443; grant the capability to the binary (setcap 'cap_net_bind_service=+ep' ./app); or set a Kubernetes or systemd option that gives it. Don't run the whole service as root to open one port (graceful shutdown and environment variables cover the neighbouring deployment details). (macOS relaxed this rule for most cases in recent versions, which is why the error appears on a Linux server and not a laptop.)
Cause 6: read-only file systems and immutable files
A related error, read-only file system, is a different code (EROFS) and means the whole mount is read-only, as with a container started with --read-only or a Kubernetes readOnlyRootFilesystem. Writing to /tmp or a mounted volume works around it. Files marked immutable with chattr +i refuse writes and deletion even for root and report operation not permitted. Both look similar in logs, and the distinction is in the message.
Finding the cause quickly
When the message appears in production and you have a shell, three commands settle most cases. id shows who the process runs as. ls -ln path shows the numeric owner, group and mode of the file. namei -l /full/path lists every directory on the way with its mode, which exposes a missing execute bit in the middle of a long path. Compare the user and groups against the owner and the mode triplet that applies. If the numbers say the access should be allowed, the denial is coming from a layer above plain modes: SELinux or AppArmor, a read-only or noexec mount, or an ACL set with setfacl (getfacl shows those). Work through them in that order, since the plain mode check explains most reports.
Handling it in code
Decide what the program should do. For an optional file it cannot read, fall back to defaults and log a warning that names the path and the user. For a required one, fail at start-up with a message that helps:
data, err := os.ReadFile(path)
switch {
case errors.Is(err, fs.ErrNotExist):
return nil, fmt.Errorf("config %q not found", path)
case errors.Is(err, fs.ErrPermission):
return nil, fmt.Errorf("config %q is not readable by uid %d: check its mode and owner", path, os.Getuid())
case err != nil:
return nil, err
}Naming the uid in the message turns a ten-minute investigation into a glance. Avoid the temptation to fix permission errors by loosening modes to 0o777; that trades a visible failure for a quiet security problem. The error values are documented in the io/fs package, and the os.Chmod documentation covers changing modes.


