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.

  1. Run ls -lan and id before changing any mode.
  2. Fix the owner or group with chown, or set the right runtime user.
  3. Apply the matrix: 700, 600, 400, 2770, 555 or 000.
  4. Run the find audit commands and review every world-writable result.
  5. 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.