OneLinersCommand workbench
Guides
Web & Proxy / Incident Response & Troubleshooting / Security

Fix NGINX 403 Forbidden: permissions, index rules, access policy and upstreams

NGINX 403 Forbidden is a response, not a diagnosis. Identify which layer refused the request, then distinguish location selection, missing directory indexes, filesystem traversal, mandatory access policy, authorization and upstream responses.

35 min7 stepsChanges system stateRevision 1
Save or explore
Save to collectionCreate a collection in the sidebar first.
0 of 7 steps completed
Goal

Restore one intended request while preserving deliberate access restrictions, using correlated request/log evidence and a narrowly scoped, reversible repair.

Supported environments
  • NGINX Supported distribution packages and upstream releases; inspect the installed build and configuration
  • Linux host Debian/Ubuntu paths shown as examples, SELinux checks apply only where enabled; AppArmor profiles vary by distribution
Prerequisites
  • Exact failing request Know the scheme, hostname, port, path, method, timestamp and expected access policy. Test a safe GET endpoint; do not replay a payment, mutation or authenticated request merely to reproduce an error. Keep session credentials out of shared logs.
  • Read access and release owner Identify who owns NGINX configuration and the application release. You need permission to inspect relevant configuration, process identity and logs. Determine whether a CDN, load balancer or application can issue the denial before touching the origin.
  • Rollback evidence Retain the last approved configuration and the exact file modes, ACLs or policy labels that a proposed repair would change. Do not recursively change ownership just because a web request failed.
Operating boundary

OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.

Full guide

What you will build

System
  • A request-specific explanation of where a 403 originated and which access boundary actually failed.
  • A reversible repair and positive/negative acceptance tests that preserve intentional restrictions.
Observable outcome
  • Separate directory index policy, Unix permissions, confinement, authentication and backend refusal.
  • Avoid the common chmod 777, autoindex and disabled-enforcement shortcuts that can expose unrelated content.

Architecture

How the parts fit together

Follow the original request across the edge, selected NGINX handler and content/backend boundary. A status alone does not reveal the component responsible.

Client and edgeProvide the original hostname, method, identity and any CDN/WAF decision before the origin.
NGINX routingSelects the virtual host and location, including index and internal-redirect processing.
Local files or upstreamEither the worker must access a resolved filesystem path or a backend/authorization service decides the response.
Policy and evidenceUnix ACLs, confinement and application access policy are validated against correlated logs and intended behavior.
  1. Capture a safe original request with correct Host/SNI and timestamp.
  2. Resolve its server/location and correlate access, error and upstream evidence.
  3. Repair one evidenced boundary through its owner.
  4. Verify intended content and a protected control through the original route.

Assumptions

  • This procedure concerns a server you administer; it is not a method for bypassing another site's authorization.
  • The commands assume a Linux host with relevant utilities installed; containerized or managed NGINX needs its own owner-approved inspection path.
  • Example outputs are illustrative and source-reviewed, not claims of testing your production NGINX instance.

Key concepts

403 is an outcome
Forbidden can be correct policy or an implementation error. The desired access behavior must be known before deciding that a denial should become success.
Traversal versus reading
The serving process needs suitable access through parent directories as well as the final file; root-shell success does not establish worker access.
Negative acceptance
A known protected path or unauthenticated case must remain restricted after the intended public request is repaired.
Set guide values0/6 ready

Fill these once. Every matching command and configuration block updates immediately; values stay in this page only.

Security and production boundaries

  • Never share raw authorization headers, cookies, private keys or unredacted configuration dumps.
  • Keep directory listing, mandatory access policy and authentication unchanged unless a narrowly reviewed policy requirement explicitly calls for that change.

Stop before continuing if

  • Stop before changing permissions when the public/private intent or actual resolved target is unknown.
  • Stop if the repair broadens access to unrelated files or requires disabling SELinux/AppArmor globally.
01

command

Reproduce a safe request with the correct hostname

read-only

Capture one bounded GET request to the public URL. A Server header or NGINX-branded error page is not proof that the origin generated the denial: an edge service or custom error handler can reuse it. Record the remote address and correlate the observation with origin logs.

Why this step matters

Testing the intended hostname preserves HTTP routing and TLS identity, which an IP-only request can silently change.

What to understand

Do not substitute curl -I without checking method behavior: HEAD and GET may follow different application rules. Do not follow redirects automatically at first, because a later destination may be the actual source of the 403. Record each observed redirect separately.

If an origin comparison is authorized, use curl --resolve with the same hostname and verified origin address so Host and TLS SNI remain meaningful. This is an isolation test, not permission to bypass production authentication or a required security proxy. Do not use -k to hide a separate certificate problem.

System changes

  • Sends one non-mutating HTTP request; it does not edit the origin or change DNS.

Syntax explained

--write-out
Prints the HTTP status and connected address without displaying a possibly sensitive response body.
--connect-timeout 5 --max-time 15
Bounds connection establishment and the entire request.
Command
Fill variables0/2 ready

Values stay on this page and are never sent or saved.

curl --silent --show-error --output /dev/null --write-out 'status=%{http_code} remote=%{remote_ip}\n' --connect-timeout 5 --max-time 15 'https://{{domain}}{{requestPath}}'
Example output / evidence
Illustrative response, not an executed production test:
status=403 remote=192.0.2.10

Checkpoint: Request identified

Continue whenThe exact safe request, expected authorization, timestamp and connected endpoint are recorded.

Stop whenReproducing the request could mutate data, expose credentials or bypass a required access boundary.

02

command

Inspect the actual server and location configuration

read-only

Validate and inspect the installed configuration privately. Determine the matching listen/server_name and location chain, including internal redirects. Trace the effective root or alias to the path named in current error evidence; do not assume the default document root is serving this hostname.

Why this step matters

Changing permissions on an assumed document root cannot repair a request that matched another virtual host or location.

What to understand

The configuration dump can contain internal addresses, authentication settings or secrets in included directives. Read it locally, redact before sharing, and do not paste the complete dump into a public issue. Check the running service's binary/configuration path when multiple installations exist.

root and alias map a request differently. Exact and regex locations, try_files, index processing and error_page can send a request to another handler. Draw the path taken by this one URI and verify it with existing logs. Avoid adding a more permissive location that accidentally overrides protection for neighboring paths.

System changes

  • Tests configuration and prints the parsed files. No reload occurs in this step.

Syntax explained

-t
Checks syntax and referenced-file accessibility; a successful parse does not prove that a worker may read application content.
-T
Performs the configuration test and dumps included configuration for private review.
Command
sudo nginx -t
sudo nginx -T
Example output / evidence
Illustrative review note:
Host: docs.example.com
Location: /downloads/
Filesystem mapping: /srv/docs/downloads/
Request is a directory; no approved index file is present
03

command

Correlate the access result with the error and upstream status

read-only

Fill the log paths from the selected server/location configuration. Read a bounded, escaped excerpt around the captured request. Correlate timestamp, request, host and available request ID; a 403 access line alone cannot distinguish a deliberate deny rule from a filesystem or application decision.

Why this step matters

The same status code covers distinct causes, so repairing from the access status alone risks removing a correct security policy.

What to understand

A directory-index denial suggests an absent index with directory listing disabled, not automatically a missing read bit. A Permission denied message directs attention to path traversal, ACLs or mandatory access policy. An explicit access-rule denial requires checking the intended allow/deny and authorization policy.

If the installed access format records upstream_status, compare it with the final status and application logs. An upstream 403 suggests the denial came from a contacted backend, but subrequests, interception and internal redirects still matter. Missing upstream fields or an empty error log are not proof that the filesystem is responsible.

System changes

  • Reads only the chosen log tails. It does not enable global debug logging or collect request bodies.

Syntax explained

tail -n 100
Bounds the excerpt; use retained logs and the exact incident interval if the request is older.
sed -n l
Escapes nonprinting characters so attacker-controlled request text is not rendered as terminal control sequences.
Command
Fill variables0/2 ready

Values stay on this page and are never sent or saved.

sudo tail -n 100 '{{accessLog}}' | sed -n l
sudo tail -n 100 '{{errorLog}}' | sed -n l
Example output / evidence
Illustrative error fragments for different branches:
directory index of "/srv/docs/downloads/" is forbidden
open() "/srv/docs/index.html" failed (13: Permission denied)
access forbidden by rule

Checkpoint: A denial branch is supported by evidence

Continue whenThe incident record identifies directory handling, filesystem access, explicit policy, upstream denial or a still-unknown branch.

Stop whenThe proposed fix is based only on the number 403 or on logs from another virtual host.

04

command

Check the resolved path and real worker identity

read-only

For a local-file request, inspect every directory component of the resolved path and the account used by workers. Directory traversal requires search permission on parents; readability of the final file alone is insufficient. Skip this branch for a denial conclusively generated by an upstream application.

Why this step matters

A narrowly identified path permission explains why a root shell succeeds while an unprivileged serving process cannot reach the file.

What to understand

Follow symlinks and inspect parent ACLs when ordinary mode bits do not explain the result. Confirm which deployment or mount owns those paths. The worker's supplementary groups and mount namespace can differ from your interactive session; a successful root read is not useful acceptance evidence.

A test executed as the worker account can help isolate discretionary permissions, but it may not reproduce the service's SELinux/AppArmor domain or sandbox. Do not conclude that mandatory access policy is healthy from a sudo -u read alone. Preserve original modes and ACLs before any narrowly approved adjustment.

System changes

  • Reads process and filesystem metadata; no chmod, chown or ACL mutation is performed.

Syntax explained

namei -l
Shows path components and their permissions to locate a traversal failure.
getfacl -p
Displays the selected path's ACL with its absolute name; inspect a specific parent separately when needed.
Command
Fill variables0/1 ready

Values stay on this page and are never sent or saved.

ps -eo user,group,args | grep '[n]ginx: worker'
namei -l '{{contentPath}}'
getfacl -p '{{contentPath}}'
Example output / evidence
Illustrative finding:
Final file is readable, but /srv/private has no traversal permission for the worker's approved group.
Repair candidate: the specific deployment path policy, not recursive chmod 777.
05

decision

Review index behavior, authentication and confinement separately

read-only

Choose the branch established by the evidence. For a directory request, determine whether an index page, application route or intentional denial is expected. For an authorization failure, compare the request with the documented identity and access policy. For a filesystem denial unexplained by modes, inspect confinement evidence for the exact process and path.

Why this step matters

Restoring one legitimate resource must not make unrelated directories, private files or unauthenticated application routes accessible as a side effect.

What to understand

Index resolution can cause an internal redirect. If the intended file is absent, repair the artifact or reviewed index setting. Enabling autoindex exposes a directory listing and is not a substitute for deploying the expected page. If the denial is intentional, document it instead of treating every non-200 response as a fault.

Check allow/deny ordering and relevant authentication modules, including auth_request. A 403 from an authorization subrequest or backend can be correct for an expired session, disallowed tenant or missing permission. Do not remove authentication or trust user-supplied forwarding headers to make the status disappear.

Where SELinux is enabled, correlate AVC records with the request and inspect existing path labels and process context; preview label restoration before applying it. On AppArmor systems, inspect the loaded profile and matching DENIED records. Correct the reviewed path/label/profile rule through its owner. Do not disable enforcement, set global permissive mode or generate broad policy from unrelated audit events.

System changes

  • This decision step authorizes no policy changes. The next step applies only the narrowly reviewed branch.
Example output / evidence
Example review outcome:
/downloads/ is intentionally not listable.
/downloads/manual.pdf is intended to be public but has a wrong deployment label.
Only the file-serving label requires correction; autoindex remains off.

Security notes

  • A temporary debug log, if genuinely required, needs a narrow scope, short duration, sensitive-data review and explicit removal.

Alternatives

  • If the upstream generated the denial, hand the correlated request to its application owner and preserve NGINX access controls.

Stop conditions

  • Stop if the desired public/private behavior is unknown or the only proposed workaround is disabling access control.
06

decision

Repair one evidenced cause and validate before reloading

caution

Save the affected configuration and its previous release identifier, then make the smallest change through the deployment owner. Examples include correcting one root/alias mapping, deploying the expected index, restoring a known label, granting the intended group traversal on one parent, or fixing application authorization under its own policy.

Why this step matters

A single reversible repair makes its effect observable and prevents a permission incident from turning into an uncontrolled access expansion.

What to understand

Do not run blanket recursive ownership or permission changes. A deploy directory can contain private keys, configuration, upload areas and executable content with different requirements. Apply only the owner-approved adjustment, retaining enough metadata to reverse it accurately.

For a configuration change, run sudo nginx -t and reload only after it succeeds, using the service manager that actually owns the running instance. Review the reload result and current error log. For an artifact, label or application-policy change, use that subsystem's validation and rollback procedure rather than restarting every layer.

System changes

  • One approved configuration, deployment metadata or application-policy change; no universal mutation is supplied because the correct target depends on evidence.
Example output / evidence
Example acceptance plan:
Change: correct one location's alias to the approved release directory
Preflight: nginx -t succeeds
Positive case: intended document returns its expected content
Negative case: private neighbor remains denied
Rollback: previous reviewed configuration revision

Stop conditions

  • Stop on syntax failure, an unexplained configuration diff, or a negative test that becomes accessible.
07

verification

Verify both the intended request and a protected control

read-only

Repeat the original safe request through the original public route and inspect current logs. Confirm the expected resource or application outcome, not merely a changed status. Then test an owner-approved protected control path or unauthenticated case to ensure the repair did not remove intentional restrictions.

Why this step matters

A positive request without a negative access-control test can falsely declare success after exposing private neighboring content.

What to understand

Validate the intended document or application behavior in addition to the status: a generic error page returned as 200 is not recovery. If a CDN caches the denial, distinguish a current origin response from a stale edge object before requesting a narrowly scoped purge.

Observe several normal requests and verify that the original error signature stops for this route while deliberate denials remain. Remove temporary diagnostic logging and record the exact cause, deployed revision and rollback evidence. Keep a separate issue for unrelated warnings uncovered during investigation.

System changes

  • Sends two approved read-only requests and checks existing evidence; does not purge caches automatically.

Syntax explained

controlPath
An owner-approved negative test whose expected response is recorded before repair, not an arbitrary private resource to probe.
Command
Fill variables0/3 ready

Values stay on this page and are never sent or saved.

curl --silent --show-error --output /dev/null --write-out 'status=%{http_code}\n' --connect-timeout 5 --max-time 15 'https://{{domain}}{{requestPath}}'
curl --silent --show-error --output /dev/null --write-out 'control_status=%{http_code}\n' --connect-timeout 5 --max-time 15 'https://{{domain}}{{controlPath}}'
Example output / evidence
Illustrative acceptance only:
status=200
control_status=403
Expected statuses must be defined by your application owner; redirects and 404 can also be intentional.

Checkpoint: Recovery with boundaries intact

Continue whenThe intended request behaves correctly, the protected control retains its expected restriction, and current logs support the diagnosed repair.

Stop whenPrivate content becomes accessible, an error is merely disguised as 200, or only an origin bypass test succeeds.

Finish line

Verification checklist

Configuration validitysudo nginx -tThe actual running instance's configuration passes parsing before reload; this does not replace request-level acceptance.
Original routecurl --silent --show-error --output /dev/null --write-out '%{http_code}\n' --max-time 15 'https://{{domain}}{{requestPath}}'The intended safe request has the owner-defined outcome through the original edge/origin route.

Recovery guidance

Common problems and safe checks

The directory URL returns 403 but a known file works.

Likely causeNo suitable directory index exists and listing is intentionally disabled, or index processing redirects to another protected handler.

Safe checks
  • Inspect the effective index and location rules.
  • Compare the actual deployed artifact with the intended index path.

ResolutionDeploy the intended index or correct the reviewed route; do not enable directory listing by default.

An open() Permission denied error persists despite readable file mode.

Likely causeA parent lacks traversal access, an ACL/mount differs, or mandatory access policy denies the worker.

Safe checks
  • Inspect the complete resolved path with namei and relevant ACLs.
  • Correlate process context and SELinux/AppArmor denials with this request.

ResolutionRepair the specific reviewed path or policy discrepancy without recursive chmod/chown or disabling enforcement.

Origin filesystem checks are clean but the client still gets 403.

Likely causeAn edge rule, access/auth subrequest, upstream application or stale cached response generated the denial.

Safe checks
  • Correlate original request, access/error records and upstream status if logged.
  • Check the approved identity and edge/application decision records.

ResolutionRepair the responsible layer while preserving NGINX filesystem and access restrictions.

Review NGINX server-block routing

After the procedure

Alternatives and next steps

Consider these alternatives

  • For an upstream denial, coordinate with the application owner using a correlated request ID; do not change local filesystem modes.
  • For virtual-host layout questions, continue with /guides/configure-nginx-server-blocks-multiple-sites. A 502 instead of 403 belongs to /guides/troubleshoot-nginx-502-bad-gateway.

Operate it safely

  • Record the exact matching route, denial source and tested negative control so the next incident starts with evidence.
  • Review deployment checks for missing index artifacts and incorrect path labels or modes, without weakening the web worker's least-privilege model.

Reference

Frequently asked questions

Does NGINX 403 Forbidden always mean bad file permissions?

No. Directory index behavior, explicit access rules, authorization subrequests, confinement, upstream applications and edge services can also refuse the request. Correlate the original request with the actual handler and logs first.

Should I fix 403 with chmod -R 777?

No. It can make private or executable files writable and does not repair routing or mandatory access policy. Identify the exact failing path component and use the owner's intended group/ACL/label policy.

Why not enable autoindex?

It exposes directory contents. If the application expects a landing page, deploy the correct index or route instead. Listing should be an intentional reviewed feature, not an incidental troubleshooting side effect.

Why does a direct IP request behave differently?

It can select a different HTTP virtual host and TLS identity, and may bypass the edge. An authorized origin comparison must preserve the intended hostname/SNI and does not replace testing the original public route.

Recovery

Rollback

Diagnosis does not require rollback. Revert only the exact approved configuration, artifact metadata or policy change if acceptance fails, using its recorded prior state and owner-managed release path.

  1. Restore the saved configuration revision or exact previous permission/ACL/label setting for the changed target. Do not recursively overwrite the entire webroot to undo a one-path repair.
  2. For NGINX configuration, test the restored configuration before reloading the correct service. For application authorization or confinement, use the responsible subsystem's reviewed rollback.
  3. Recheck the original request, the protected control and current logs. Preserve evidence of any exposure as an incident rather than considering the rollback alone sufficient closure.

Evidence

Sources and review

Verified 2026-09-09Review due 2026-12-09
NGINX request routing and core directivesofficialNGINX index processingofficialNGINX access rulesofficialNGINX access loggingofficialNGINX upstream response statusofficialNGINX authorization subrequestsofficialNGINX configuration validation and reloadofficialLinux namei path inspectionofficialcurl request optionsofficialUbuntu AppArmorofficialRed Hat SELinux troubleshootingofficial