Reverse Proxy Access Logs

Updated

NetBird logs every request and connection that passes through your reverse proxy services, including both HTTP and L4 (TCP, UDP, TLS) traffic. Access logs provide visibility into who is accessing your services, from where, and whether requests were allowed or denied. This is useful for auditing, debugging, and monitoring traffic to your publicly exposed services.

Viewing access logs

Access logs are available in the NetBird dashboard under Reverse Proxy > Access Logs. This view displays a table of all HTTP requests and L4 connections that have passed through your reverse proxy services, with filters to narrow down results by time range, status, or other fields.

Proxy Events table showing reverse proxy access log entries

You can also retrieve access logs programmatically using the API:

GET /api/events/proxy

What access logs capture

Every log entry (HTTP and L4) shares a common set of fields. Some fields are only meaningful for certain protocols.

FieldDescriptionHTTPL4 (TCP/UDP/TLS)
TimestampWhen the request or connection occurredYesYes
Protocolhttp, tcp, udp, or tlsYesYes
MethodHTTP method (GET, POST, etc.). For L4, displays the protocol name.YesProtocol name
Host / URLDomain and path for HTTP. Host and listen port for L4.host/pathhost:port
Status CodeHTTP status code (200, 401, 403, 500, etc.)YesEmpty
DurationRequest or connection duration in millisecondsYesYes
Bytes UploadedBytes sent from client to backendYesYes
Bytes DownloadedBytes sent from backend to clientYesYes
Source IPThe client's IP addressYesYes
LocationCountry, city, and subdivision based on source IP geolocationYesYes
Auth MethodRaw auth_method_used value: oidc (shown as SSO in the dashboard), password, pin, header, or a target access action such as path_bypass or path_block. When an access restriction rejects the request, carries the restriction code instead (e.g. ip_restricted, crowdsec_ban). Omitted from the API response when emptyYesRestriction code on denials
UserThe authenticated user's ID, set when oidc authentication was used. Omitted from the API response when emptyYesN/A
ReasonDerived from the HTTP response status: Authentication failed for 401/403, or Request failed for other 4xx/5xx responses. This includes responses from the backend. Omitted when empty. For specific restriction codes, see Deny reasonsYesOmitted
MetadataAdditional decision details. Target access records access_action as bypass, block, or invalid_path (an ambiguous request path rejected with 400); CrowdSec records its mode and verdict hereYesYes

Understanding log entries

HTTP log entries

Use the status code together with auth_method_used and metadata to interpret HTTP entries:

  • Allowed requests: successful requests show a 2xx status code along with the authentication method used to access the service. A target that bypasses authentication records path_bypass. The dashboard marks these entries with a small open-lock icon in the User column, at the right end of the row, labeled Auth Bypassed on hover.
  • Redirects: 3xx responses can come from the backend or the proxy's authentication flow. The reason field is omitted.
  • Unauthorized or forbidden responses: 401 and 403 responses have reason set to Authentication failed. These can originate from proxy authentication, access restrictions, blocked target paths, or the backend. A blocked target path records path_block in auth_method_used; the dashboard shows a shield icon labeled Path Blocked on hover. Access restrictions record the codes listed under Deny reasons.
  • Other errors: other 4xx responses and 5xx responses have reason set to Request failed. They can originate from the backend or the proxy and do not imply that the request was authenticated.

L4 log entries

L4 entries are logged when the connection closes and record the total bytes transferred in each direction and the connection duration. L4 entries do not have HTTP status codes.

Denied L4 connections (blocked by access restrictions) are logged immediately. L4 entries carry no reason value, so the restriction code identifies the denial. Since L4 services do not support authentication, denials come from access restrictions only.

Deny reasons

The following deny reasons identify why a connection was rejected. Note that for HTTP services these values are not carried in the entry's reason field: see the note below the table.

ReasonDescription
path_blockThe matching HTTP target is configured to block access
ip_restrictedThe client IP was blocked by a CIDR access restriction
country_restrictedThe client's country was blocked by a country access restriction
geo_unavailableCountry restrictions are configured but the GeoIP database is unavailable (fail-closed)
crowdsec_banThe client IP has a CrowdSec ban decision
crowdsec_captchaThe client IP has a CrowdSec captcha decision
crowdsec_throttleThe client IP has a CrowdSec throttle decision
crowdsec_unavailableCrowdSec enforce mode is active but the bouncer has not completed its initial sync (fail-closed)

All CrowdSec decision types (ban, captcha, throttle) result in a connection denial in enforce mode. The proxy does not serve captcha challenges or apply rate limiting: the decision type is recorded for informational purposes only.

When CrowdSec is in observe mode, the verdict appears in the log metadata and CrowdSec adds no deny reason of its own (the connection is allowed). In the dashboard, these entries render with an observe-mode badge on the reason cell and show the underlying decision type (ban, captcha, throttle, unavailable) on hover. This lets you audit what CrowdSec would block without affecting traffic. For a self-test workflow, see Testing the integration.

Use cases

Access logs support several operational and security workflows:

  • Security auditing - Review who accessed sensitive services, which authentication method they used, and from which geographic locations. This helps identify unauthorized access attempts and verify that authentication policies are working as expected.
  • Debugging - Trace failed requests to understand authentication or routing issues. The status code, duration, and reason fields help pinpoint whether problems originate from authentication configuration, target availability, or network connectivity.
  • Monitoring - Track usage patterns across your services. Identify which services receive the most traffic, peak usage times, and unusual access patterns that may warrant investigation.
  • Compliance - Maintain records of service access for regulatory requirements. Access logs provide an auditable trail of who accessed what, when, and from where.

Retention and Cleanup

For NetBird Cloud, access logs are retained for 7 days. Self-hosted deployments can configure retention in the management server's configuration file.

For standalone management, set ReverseProxy in management.json:

{
    "ReverseProxy": {
        "AccessLogRetentionDays": 7,
        "AccessLogCleanupIntervalHours": 24
    }
}

For the combined server, configure retention under server.reverseProxy in config.yaml. If proxy trust settings are already configured, add or update the retention keys alongside them. Preserve existing trustedHTTPProxies, trustedPeers, and trustedHTTPProxiesCount values, including those written by the getting-started script:

server:
  reverseProxy:
    # Keep your existing proxy trust settings here.
    accessLogRetentionDays: 7
    accessLogCleanupIntervalHours: 24

Older installations and migrated configurations may have no server.reverseProxy block or proxy trust settings. In that case, create the block if needed. The example below uses 172.30.0.10/32, the bundled Traefik container's address in the Docker Compose configuration generated by the quickstart and migration scripts. If you use a different upstream proxy or changed its container address, replace the example addresses with that proxy's actual source address or CIDR as seen by management.

server:
  reverseProxy:
    trustedPeers:
      - "172.30.0.10/32"
    trustedHTTPProxies:
      - "172.30.0.10/32"
    accessLogRetentionDays: 7
    accessLogCleanupIntervalHours: 24
  • accessLogRetentionDays - Retention in days. An omitted value or 0 uses the default of 7 days. A negative value disables automatic cleanup.
  • accessLogCleanupIntervalHours - Time between cleanup passes in hours. An omitted or non-positive value uses the default of 24 hours. When cleanup is enabled, it also runs at startup.

Storage

Starting with v0.82.0, self-hosted deployments keep access logs on their own database connection, separate from the rest of the management data. By default, they follow the main store engine:

  • SQLite: a separate access_logs.db file in the data directory, next to store.db. A backup of the data volume covers both.
  • PostgreSQL: the main database, through a separate connection pool.

To store access logs elsewhere, such as a dedicated PostgreSQL database, see Access Log Store Settings or the NB_ACCESS_LOG_* environment variables.

When access logs move to another database, existing entries stay behind: they no longer appear in the dashboard, and the retention cleanup no longer removes them. With the default 7-day retention, this affects at most the last week of history. To keep them, copy them to the new database, then drop the old table to reclaim the space. Copied entries older than accessLogRetentionDays are removed by the next cleanup run.

Upgrading a SQLite deployment

Before v0.82.0, access logs were stored in store.db. After the upgrade, new entries are written to access_logs.db, which the server creates on its first start.

The netbird-server image doesn't include sqlite3, so the steps below run it from a throwaway container. Find the volume name with docker volume ls (it ends in netbird_data).

  1. From the install directory, stop the NetBird containers and back up the data volume:

    docker compose stop
    
  2. Optional: copy the old entries into access_logs.db:

    docker run --rm -v VOLUME_NAME:/data alpine sh -c \
      "set -o pipefail && apk add -q sqlite && sqlite3 -cmd '.headers on' -cmd '.mode insert access_log_entries' /data/store.db 'SELECT * FROM access_log_entries;' | sqlite3 /data/access_logs.db"
    
  3. Drop the old table and compact store.db:

    docker run --rm -v VOLUME_NAME:/data alpine sh -c \
      "apk add -q sqlite && sqlite3 /data/store.db 'DROP TABLE IF EXISTS access_log_entries; VACUUM;'"
    
  4. Start the containers:

    docker compose start
    

Moving to a dedicated PostgreSQL database

PostgreSQL deployments keep their history on upgrade, because access logs stay in the main database. Once you point access logs at another database with server.accessLogStore.dsn or NB_ACCESS_LOG_POSTGRES_DSN, new entries go there and the server creates the table on startup. All steps below can run while the server is up, using pg_dump and psql that match your server's major version. <main_store_dsn> is the value of server.store.dsn, and <access_log_dsn> is the value of server.accessLogStore.dsn.

  1. Optional: copy the old entries into the new database:

    pg_dump --data-only --table=access_log_entries -d "<main_store_dsn>" | psql -d "<access_log_dsn>"
    
  2. Verify that new entries no longer reach the main database. Run this query against both databases:

    psql -d "<main_store_dsn>" -c "SELECT count(*), max(timestamp) FROM access_log_entries;"
    psql -d "<access_log_dsn>" -c "SELECT count(*), max(timestamp) FROM access_log_entries;"
    

    The newest entry in the main database should predate the switch, and its count should stay the same when you run the query again after new traffic through a service. If you copied the old entries, the dedicated database should hold at least as many entries as the main one, minus any copied entries older than accessLogRetentionDays that the cleanup has already removed.

  3. Back up the old table:

    pg_dump --table=access_log_entries -d "<main_store_dsn>" -f access_log_entries.sql
    

    The file includes the table definition, so psql -d "<main_store_dsn>" -f access_log_entries.sql restores the table if you need it back.

  4. Drop the old table from the main database:

    psql -d "<main_store_dsn>" -c "DROP TABLE access_log_entries;"