Fixing Linux Permission Denied Errors & Decoding Base64 Logs Locally (Without chmod 777 or Third-Party Web Servers)
Two shortcuts show up in almost every infrastructure audit I review. The first is a directory set to 777 because a deploy failed once. The second is an internal log line pasted into a random online decoder because someon
Two shortcuts show up in almost every infrastructure audit I review. The first is a directory set to 777 because a deploy failed once. The second is an internal log line pasted into a random online decoder because someone needed an answer quickly.
Both shortcuts solve the immediate problem and create a compliance finding. This guide covers the proper fix for the most common Permission denied errors, a least-privilege reference matrix, and how to decode logs and tokens without them leaving your machine.
Why do chmod 777 and 666 fail production audits?
Mode 777 gives every account on the host read, write and execute access, and 666 gives every account read and write. Least-privilege standards such as the CIS Benchmarks flag world-writable files because any process on the host can change them. The fix is almost always ownership, not wider permissions.
Where 777 usually comes from
Nobody chooses 777 as a design decision. It appears when a permission error blocks a deploy and widening access makes the error go away. Three scenarios cause most of it.
Docker bind mounts. The container process runs as one UID, while the host directory belongs to another. The app reports EACCES, and someone runs chmod -R 777 ./data on the host. Check the numbers first:
ls -lan ./data
docker run --rm myimage id
If the container runs as UID 999 and the directory belongs to UID 1000, fix the ownership or the runtime user:
sudo chown -R 999:999 ./data
# or run the container as the host user
docker run --user "$(id -u):$(id -g)" -v "$PWD/data:/var/lib/app" myimage
Nginx and PHP-FPM sockets. The Nginx error log shows a line like this:
connect() to unix:/run/php/php8.2-fpm.sock failed (13: Permission denied) while connecting to upstream
Setting the socket to 777 hides the problem until the next FPM restart recreates it. The durable fix lives in the pool config, usually /etc/php/8.2/fpm/pool.d/www.conf:
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
Restart FPM, then confirm the result with stat -c "%a %U:%G %n" /run/php/php8.2-fpm.sock. You should see 660 www-data:www-data.
CI/CD runner workspaces. A containerized build step on a self-hosted runner writes files as root. The next checkout fails because the runner user cannot delete them. Instead of adding chmod -R 777 to the pipeline, run the build container as the runner's UID, or add a cleanup step that restores ownership with chown.
The 9-bit math behind the finding
A Unix mode has nine permission bits in three groups: owner, group and others. Each group uses the same weights.
r = 4 w = 2 x = 1 digit = r + w + x
777 -> 111 111 111 -> rwx rwx rwx
770 -> 111 111 000 -> rwx rwx ---
666 -> 110 110 110 -> rw- rw- rw-
644 -> 110 100 100 -> rw- r-- r--
Compare 777 with 770. The only difference is the last group of three bits, the "others" scope. Those bits cover every account that is neither the owner nor in the group: unrelated services, other tenants on a shared host, and automation you forgot was running.
When "others" can write a configuration file, any of those processes can change how your application behaves. That is why auditors treat world-writable files as a finding, even when nothing has gone wrong yet.
Auditing a host for world-writable files
These read-only commands list what an auditor would flag. They stay on one filesystem and skip /proc:
# world-writable regular files
sudo find / -xdev -type f -perm -0002 -printf '%m %u:%g %p\n' 2>/dev/null
# world-writable directories without the sticky bit
sudo find / -xdev -type d -perm -0002 ! -perm -1000 -printf '%m %u:%g %p\n' 2>/dev/null
Directories such as /tmp are world-writable by design, but they carry the sticky bit (1777). That bit stops users from deleting each other's files, which is why the second command excludes it.
What is the least-privilege permission matrix?
Give each file type the smallest mode that still lets its owner and service do their job. For most production hosts, six modes cover nearly everything. The table below maps each one to the files it belongs on.
| Mode |
ls -l view |
Binary | Use it for |
|---|---|---|---|
700 |
drwx------ |
111 000 000 |
~/.ssh and isolated per-user folders |
600 |
-rw------- |
110 000 000 |
~/.ssh/id_ed25519, .env files, TLS private keys |
400 |
-r-------- |
100 000 000 | Cloud .pem keypairs, immutable config files |
770 / 2770
|
drwxrwx--- / drwxrws---
|
111 111 000 | Shared deployment directories (deploy:www-data) |
555 |
-r-xr-xr-x |
101 101 101 | Read-only executable binaries |
000 |
---------- |
000 000 000 | Locked archive files |
For a visual version of each row, the chmod 700 octal and symbolic permission breakdown and the chmod 600 SSH key permission calculator show the bits, the symbolic string and the matching commands side by side.
Applying the matrix from the CLI
SSH directory and keys. OpenSSH refuses keys that other accounts can read, so these two modes matter in practice:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519 ~/.ssh/config
stat -c "%a %n" ~/.ssh ~/.ssh/id_ed25519
# 700 /home/deploy/.ssh
# 600 /home/deploy/.ssh/id_ed25519
Environment files and TLS keys. The service account that reads them should own them:
sudo chown app:app /srv/app/.env
sudo chmod 600 /srv/app/.env /etc/ssl/private/app.key
Cloud keypairs. Use 400 so even the owner cannot overwrite the key by accident:
chmod 400 ~/keys/staging-ec2.pem
Shared deployment directories with setgid. Here the deploy user owns the tree and the www-data group can work in it. The leading 2 sets the setgid bit, so new files inherit the www-data group automatically:
sudo install -d -o deploy -g www-data -m 2770 /srv/releases
ls -ld /srv/releases
# drwxrws--- 2 deploy www-data 4096 Nov 10 09:12 /srv/releases
The lowercase s in the group position confirms setgid is active. Pair it with a umask of 0007 for the deploy process, so new files are group-writable but closed to others.
Binaries and archives. Mode 555 lets everyone run a binary but nobody edit it. Mode 000 parks an archive until someone deliberately restores access:
sudo chmod 555 /usr/local/bin/report-agent
sudo chmod 000 /srv/archive/2025-q4.tar.gz
Root still bypasses read and write checks, and the owner can always run chmod again. Treat 000 and 555 as guardrails against mistakes, not as a security boundary.
When I need to convert between octal and symbolic notation quickly, I use this client-side Linux Chmod Calculator. The reasoning behind each mode is covered in the complete Linux chmod 000, 400, 555, 600, 666, and 770 permissions guide.
How do you decode Base64 logs and JWTs without sending them to a server?
Decode them with local tools: base64 and iconv in a shell, or browser APIs that never make a network request. A server-side decoder receives everything you paste as a request body. After that, your data sits under someone else's logging and retention policy.
What actually leaves your machine
Diagnostic data is more sensitive than it looks. A Windows Event 4625 line includes account names, workstation names and source IP addresses. Nginx and syslog entries reveal internal hostnames, paths and request patterns.
Even a benign PowerShell automation script, stored as an -EncodedCommand string, can contain server names and share paths. A staging Bearer JWT carries user IDs and roles, and it may still be valid when you paste it. Ad-supported decoder sites also tend to load third-party scripts, and some of those record what you type into form fields.
Decoding UTF-16LE Base64 in bash
PowerShell's -EncodedCommand expects Base64 of UTF-16LE text, not UTF-8. That is why a plain base64 -d prints text with null bytes between the letters. Convert the encoding with iconv:
echo 'RwBlAHQALQBTAGUAcgB2AGkAYwBlAA==' | base64 -d | iconv -f UTF-16LE -t UTF-8
# Get-Service
Gzip-compressed Base64 blobs need one more step:
echo "$BLOB" | base64 -d | gunzip
To inspect a JWT's claims, decode the middle segment. JWTs use URL-safe Base64 without padding, so Python handles it more reliably than base64 -d:
python3 -c "import sys,base64,json; p=sys.argv[1].split('.')[1]; print(json.dumps(json.loads(base64.urlsafe_b64decode(p+'='*(-len(p)%4))), indent=2))" "$JWT"
Doing the same work inside a browser tab
Modern browsers ship the same capabilities as built-in APIs. Code that uses them runs entirely in the page, with no request carrying your input.
// UTF-16LE Base64 -> text
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
const text = new TextDecoder('utf-16le').decode(bytes);
// gzip Base64 -> text
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'));
const unzipped = await new Response(stream).text();
// HS256 JWT signature check with a staging secret that never leaves the tab
const [h, p, s] = jwt.split('.');
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
const sig = Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
const valid = await crypto.subtle.verify('HMAC', key, sig, new TextEncoder().encode(`${h}.${p}`));
TextDecoder handles character encodings, DecompressionStream handles gzip and deflate, and crypto.subtle handles signature checks. Together they cover most log and token inspection work without a backend.
You can confirm any tool behaves this way. Open DevTools, clear the Network tab, paste your input and run the tool. If no request contains your data, and the tool still works with Wi-Fi off, the processing is local.
That is the approach behind the offline PowerShell Base64 and gzip Log De-obfuscator, which detects the encoding layers and unwraps them in the tab. The browser-local JWT Inspector decodes headers and claims and flags risky settings such as alg: none the same way.
A short checklist for your next audit
Fix ownership before touching permissions, and keep diagnostic data on hardware you control. Most Permission denied errors are UID or group mismatches, not missing bits.
- Run
ls -lanandidbefore changing any mode. - Fix the owner or group with
chown, or set the right runtime user. - Apply the matrix: 700, 600, 400, 2770, 555 or 000.
- Run the
findaudit commands and review every world-writable result. - Decode logs and tokens locally, then confirm in DevTools that nothing left the tab.
All of the browser utilities mentioned here are collected on LocalSecTools, a set of client-side tools that run entirely in the browser.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.