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.

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.
| Field | Description | HTTP | L4 (TCP/UDP/TLS) |
|---|---|---|---|
| Timestamp | When the request or connection occurred | Yes | Yes |
| Protocol | http, tcp, udp, or tls | Yes | Yes |
| Method | HTTP method (GET, POST, etc.). For L4, displays the protocol name. | Yes | Protocol name |
| Host / URL | Domain and path for HTTP. Host and listen port for L4. | host/path | host:port |
| Status Code | HTTP status code (200, 401, 403, 500, etc.) | Yes | Empty |
| Duration | Request or connection duration in milliseconds | Yes | Yes |
| Bytes Uploaded | Bytes sent from client to backend | Yes | Yes |
| Bytes Downloaded | Bytes sent from backend to client | Yes | Yes |
| Source IP | The client's IP address | Yes | Yes |
| Location | Country, city, and subdivision based on source IP geolocation | Yes | Yes |
| Auth Method | Raw 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 empty | Yes | Restriction code on denials |
| User | The authenticated user's ID, set when oidc authentication was used. Omitted from the API response when empty | Yes | N/A |
| Reason | Derived 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 reasons | Yes | Omitted |
| Metadata | Additional 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 here | Yes | Yes |
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
2xxstatus code along with the authentication method used to access the service. A target that bypasses authentication recordspath_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:
3xxresponses can come from the backend or the proxy's authentication flow. Thereasonfield is omitted. - Unauthorized or forbidden responses:
401and403responses havereasonset toAuthentication failed. These can originate from proxy authentication, access restrictions, blocked target paths, or the backend. A blocked target path recordspath_blockinauth_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
4xxresponses and5xxresponses havereasonset toRequest failed. They can originate from the backend or the proxy and do not imply that the request was authenticated.
A backend can return 401 or 403 after proxy authentication succeeds. The log still says Authentication failed, so that label alone does not establish that the proxy denied access. Check auth_method_used, metadata, and the backend's logs to identify the source.
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.
| Reason | Description |
|---|---|
path_block | The matching HTTP target is configured to block access |
ip_restricted | The client IP was blocked by a CIDR access restriction |
country_restricted | The client's country was blocked by a country access restriction |
geo_unavailable | Country restrictions are configured but the GeoIP database is unavailable (fail-closed) |
crowdsec_ban | The client IP has a CrowdSec ban decision |
crowdsec_captcha | The client IP has a CrowdSec captcha decision |
crowdsec_throttle | The client IP has a CrowdSec throttle decision |
crowdsec_unavailable | CrowdSec 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.
For HTTP services, the deny code from the table above is recorded in the auth_method_used field, and the entry's reason field carries a synthesized generic value rather than the specific code. This applies to every access restriction, not only CrowdSec:
{ "status_code": 403, "reason": "Authentication failed", "auth_method_used": "ip_restricted" }
{
"status_code": 403,
"reason": "Authentication failed",
"auth_method_used": "crowdsec_ban",
"metadata": { "crowdsec_verdict": "crowdsec_ban" }
}
When reading entries through GET /api/events/proxy, match on auth_method_used (and metadata.crowdsec_verdict for CrowdSec specifically) rather than reason.
Observe-mode entries carry the normal status code and record both crowdsec_mode and crowdsec_verdict in metadata. Because the connection is allowed, CrowdSec itself contributes no reason, but the field can still be populated by a later stage of the request such as an authentication or backend failure. Treat metadata.crowdsec_mode as the signal that an entry is an observe-mode verdict, not the absence of reason.
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
The current combined-server configuration loader applies this block only when trustedPeers or trustedHTTPProxies is nonempty, or trustedHTTPProxiesCount is greater than zero. A retention-only block is ignored, leaving the default retention in effect. These trust settings control which forwarded client IPs management accepts; use settings appropriate for your deployment rather than trusting every source to activate retention.
- accessLogRetentionDays - Retention in days. An omitted value or
0uses 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.dbfile in the data directory, next tostore.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).
-
From the install directory, stop the NetBird containers and back up the data volume:
docker compose stop -
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" -
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;'" -
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.
-
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>" -
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
accessLogRetentionDaysthat the cleanup has already removed. -
Back up the old table:
pg_dump --table=access_log_entries -d "<main_store_dsn>" -f access_log_entries.sqlThe file includes the table definition, so
psql -d "<main_store_dsn>" -f access_log_entries.sqlrestores the table if you need it back. -
Drop the old table from the main database:
psql -d "<main_store_dsn>" -c "DROP TABLE access_log_entries;"
Drop the table only after every management instance runs with the dedicated database configured. Without server.accessLogStore.dsn, access logs are still stored in the main database's access_log_entries table.
Related pages
- Reverse Proxy Overview - learn how to create and manage reverse proxy services
- Audit Events Logging - view management-level events such as configuration changes and user activity
- Traffic Events Logging - monitor network-level traffic events across your NetBird mesh

