return answers before allow/deny is applied

Your access list is syntactically perfect and it is never consulted. return runs in an earlier phase and has already answered the request.

MEDIUM return_bypasses_allow_deny 5 min read Updated: August 2026

What Gixy reports

[return_bypasses_allow_deny] Return directive bypasses allow/deny restrictions in the same context.
  Severity: MEDIUM
  Reason: allow/deny do not restrict access to responses produced by
          return in the same scope.

The mistake

location /health {
    allow 10.0.0.0/8;
    deny all;

    return 200 "ok";
}

That looks airtight. It is not: anyone on the internet gets 200 ok. The access list is correct, complete, and never reached.

Why: nginx phases

nginx processes a request through an ordered series of phases. Two of them matter here, and they do not run in the order the config file suggests:

  1. NGX_HTTP_REWRITE_PHASE -- where return and rewrite live
  2. NGX_HTTP_ACCESS_PHASE -- where allow and deny live

Rewrite comes first. When return fires it terminates the request and sends the response immediately; the access phase never runs. Position in the config file is irrelevant -- moving return below deny all; changes nothing, because nginx is not reading your block top to bottom at request time.

This is the same root cause as "if is evil". Most of nginx's genuinely surprising behaviour comes from directives from different modules running in different phases, in an order that has nothing to do with how the file is written.

The fix

Put the restriction somewhere the access phase actually runs, then reach the canned response through an internal redirect. A named location does this cleanly:

http {
    open_file_cache max=10000 inactive=60s;
    open_file_cache_errors on;

    server {
        location /health {
            allow 10.0.0.0/8;
            deny all;

            try_files /nonexistent @health;
        }

        location @health {
            return 200 "ok";
        }
    }
}

try_files runs in the content phase, after access has been evaluated, so a refused client gets its 403 before the internal redirect is considered.

Pair it with open_file_cache. A bare try_files trips a different Gixy check — try_files_is_evil_too — because every request pays a filesystem lookup per candidate. Add open_file_cache max=10000 inactive=60s; and open_file_cache_errors on; in the http block so you fix this finding without creating that one.

If the response body is genuinely static, serving a real file is simpler and sidesteps both findings entirely:

location /health {
    allow 10.0.0.0/8;
    deny all;

    root /var/www/health;   # contains a file named "index.html"
}

Verify the fix

gixy /etc/nginx/nginx.conf

# From a disallowed address -- this is the whole test:
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/health
# before the fix: 200
# after the fix:  403

When this is a false positive

When the return is the thing you want everyone to get. A redirect block is the usual example:

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

Nobody is being protected here and no access list is present, so Gixy has nothing to flag. The finding only appears when allow or deny is in scope -- which means someone wrote an access rule that they believed was doing something. It is worth a look every time.

Reference