From 5a4dc22c02df35f535f9bbc0d547f34702db3ad0 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Thu, 2 Nov 2023 11:15:41 +0100 Subject: [PATCH] Add authentication documentation Signed-off-by: DL6ER --- docs/abbreviations.md | 5 + docs/api/auth.md | 256 ++++++++++++++++++++++++++++++++++++++++++ docs/api/index.md | 47 +++++--- mkdocs.yml | 1 + 4 files changed, 293 insertions(+), 16 deletions(-) create mode 100644 docs/api/auth.md diff --git a/docs/abbreviations.md b/docs/abbreviations.md index 53cebce..af4e99c 100644 --- a/docs/abbreviations.md +++ b/docs/abbreviations.md @@ -1,5 +1,7 @@ +*[2FA]: Two-factor authentication *[API]: Application Programming Interface (a set of subroutine definitions, protocols, and tools for building application software) +*[CSRF]: Cross-site request forgery *[DNS]: Domain Name Service (decentralized naming system for computers, services, or other resources connected to the Internet) *[DnyDNS]: Dynamic DNS record pointing to a frequently changing IP address *[DHCP]: Dynamic Host Configuration Protocol (network management protocol for configuring Internet Protocol version 4 (IPv4) hosts with IP addresses) @@ -35,11 +37,14 @@ *[Regex]: Regular expression *[regex]: Regular expression *[SQLite3]: Database engine that handles SQL databases in a file +*[SID]: Session ID *[ID]: Identifier *[SSH]: Secure Shell is a cryptographic network protocol for operating network services securely over an unsecured network *[TFTP]: Trivial File Transfer Protocol is a simple lockstep File Transfer Protocol which allows a client to get a file from or put a file onto a remote host *[TTL]: Time-To-Live is a mechanism that limits the lifespan or lifetime of data in a computer or network +*[TOTP]: Time-based One-Time Password *[NAT]: Network address translation *[UTF-8]: 8-bit Unicode Transformation Format - a character encoding format capable of encoding all known 1,112,064 valid Unicode characters *[URL]: Uniform Resource Locator, commonly known as "web address" *[REST]: Representational State Transfer - a software architecture for distributed systems like the World Wide Web (WWW) +*[XSS]: Cross-site scripting diff --git a/docs/api/auth.md b/docs/api/auth.md new file mode 100644 index 0000000..631cfcb --- /dev/null +++ b/docs/api/auth.md @@ -0,0 +1,256 @@ +# API Reference: Authentication + +Authentication is required for most API endpoints. The Pi-hole API uses a session-based authentication system. This means that you will not be able to use a static token to authenticate your requests. Instead, you will be given a session ID (SID) that you will have to use. If you didn't set a password for your Pi-hole, you don't have to authenticate your requests. + +To get a session ID, you will have to send a `POST` request to the `/api/auth` endpoint with a payload either containing your password. Note that is also possible to use an application password instead of your regular password, e.g., if you don't want to put your password in your scripts or if you have 2FA enabled for your regular password. Application passwords can be generated in the web interface on the settings page. + + +???+ example "Authentication with password" + + === "cURL" + + ``` bash + curl -k -X POST "https://pi.hole/admin/api/auth" --data '{"password":"your-password"}' + ``` + + === "Python 3" + + ``` python + import requests + + url = "https://pi.hole/admin/api/auth" + payload = {"password": "your-password"} + + response = requests.request("POST", url, json=payload, verify=False) + + print(response.text) + ``` + + **Parameters** + + Specify either your password or an application password as `password` in the payload. + +???+ success "Success response" + + Response code: `HTTP/1.1 200 OK` + + ```json + { + "session": { + "valid": true, + "totp": false, + "sid": "vFA+EP4MQ5JJvJg+3Q2Jnw=", + "csrf": "Ux87YTIiMOf/GKCefVIOMw=", + "validity": 300 + }, + "took": 0.0002 + } + ``` + +???+ failure "Error response" + + Response code: `HTTP/1.1 401 - Unauthorized` + + ```json + { + "error": { + "key": "unauthorized", + "message": "Unauthorized", + "hint": null + }, + "took": 0.003 + } + ``` + +On success, this will return a JSON object containing the session ID (SID) and the time until your session expires (in seconds). +You can use this SID from this point to authenticate your requests to the API. + +## Use the SID to access API endpoints + +Once you have a valid SID, you can use it to authenticate your requests. You can do this in four different ways: + +1. In the request URI: `http://pi.hole/admin/api.php?sid=vFA+EP4MQ5JJvJg+3Q2Jnw=` +2. In the payload of your request: `{"sid":"vFA+EP4MQ5JJvJg+3Q2Jnw="}` +3. In the `X-FTL-SID` header: `X-FTL-SID: vFA+EP4MQ5JJvJg+3Q2Jnw=` +4. In the `sid` cookie: `Cookie: sid=vFA+EP4MQ5JJvJg+3Q2Jnw=` + +Note that when using cookie-based authentication, you will also need to send a `X-FTL-CSRF` header with the CSRF token that was returned when you authenticated. This is to prevent a certain kind of identify theft attack the Pi-hole API is immune against. + +???+ example "Authentication with SID" + + === "cURL" + + ``` bash + # Example: Authentication with SID in the request URI + curl -k -X GET "https://pi.hole/api/dns/blocking?sid=vFA+EP4MQ5JJvJg+3Q2Jnw=" + ``` + + === "Python 3" + + ``` python + # Example: Authentication with SID in the request header + import requests + + url = "https://pi.hole/api/dns/blocking" + payload = {} + headers = { + "X-FTL-SID": "vFA+EP4MQ5JJvJg+3Q2Jnw=", + "X-FTL-CSRF": "Ux87YTIiMOf/GKCefVIOMw=" + } + + response = requests.request("GET", url, headers=headers, data=payload, verify=False) + + print(response.text) + ``` + + **Parameters** + + Specify the SID in one of the four ways described above. + + **Headers** + + If you use cookie-based authentication, you will also need to send a `X-FTL-CSRF` header with the CSRF token that was returned when you authenticated. + +## Authentication with 2FA + +If you have 2FA enabled for your Pi-hole, you will need to provide a TOTP token in addition to your password. You can generate this token using any TOTP app, e.g., [Google Authenticator](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2) or [Authy](https://authy.com/download/). The Pi-hole API will return a `totp` flag in the response to indicate whether 2FA is enabled on your Pi-hole. + +???+ example "Authentication with 2FA" + + === "cURL" + + ``` bash + curl -k -X POST "https://pi.hole/admin/api/auth" --data '{"password":"your-password", "totp":"123456"}' + ``` + + === "Python 3" + + ``` python + import requests + + url = "https://pi.hole/admin/api/auth" + payload = { + "password": "your-password", + "totp": 123456 + } + + response = requests.request("POST", url, json=payload, verify=False) + + print(response.text) + ``` + + **Parameters** + + Specify your password as `password` and the TOTP token you got from your TOTP app as `totp` in the payload. + +## Handling Authentication Errors + +When authenticating, it's possible to encounter errors. These can occur due to various reasons such as incorrect password, server issues, or network problems. Here's how you can handle these errors: + +When you send a `POST` request to the `/api/auth` endpoint, the server will respond with a status code. If the authentication is successful, you will receive a `200 OK` status code. If there's an error, you will receive a different status code. Here are some common ones: + +- `400 Bad Request`: This usually means that the JSON data in the request is not formatted correctly or the required fields are missing. +- `401 Unauthorized`: This means that the password provided is incorrect. +- `500 Internal Server Error`: This indicates that something went wrong on the server side (e.g., a system running out of memory). + +In addition to the status code, the server will also return a JSON object with more information about the error, e.g., + +???+ failure "Error response" + + Response code: `HTTP/1.1 400 - Bad Request` + + ```json + { + "error": { + "key": "bad_request", + "message": "No password found in JSON payload", + "hint": null + }, + "took": 0.0001 + } + ``` + + or + + ``` json + { + "error": { + "key": "bad_request", + "message": "Field password has to be of type 'string'", + "hint": null + }, + "took": 0.0003 + } + ``` + +## Session Expiration + +The session ID (SID) has a limited lifespan. Each successful request to the API will extend the lifespan of the SID. However, if there is a prolonged period of inactivity, the SID will expire and you will need to re-authenticate to obtain a new one. The timeout until the SID expires is configurable via a config setting. + +## Logging Out + +To end your session before the SID expires, you can send a `DELETE` request to the `/api/auth` endpoint. This will invalidate your current SID, requiring you to login again for further requests. + +???+ example "Logout" + + === "cURL" + + ``` bash + # Example: Logout with SID in the request URI + curl -k -X DELETE "https://pi.hole/admin/api/auth?sid=vFA+EP4MQ5JJvJg+3Q2Jnw=" + ``` + + === "Python 3" + + ``` python + # Example: Logout with SID in the request header + import requests + + url = "https://pi.hole/admin/api/auth" + payload = {} + headers = { + "X-FTL-SID": "vFA+EP4MQ5JJvJg+3Q2Jnw=" + } + + response = requests.request("DELETE", url, headers=headers, data=payload, verify=False) + + print(response.text) + ``` + + **Parameters** + + Specify the SID in one of the four ways described above. + +???+ success "Success response" + + Response code: `HTTP/1.1 410 - Gone` + + No content + +Remember, it's important to manage your sessions properly to maintain the security of your Pi-hole API. + +## Rate Limiting + +The Pi-hole API implements a rate-limiting mechanism to prevent abuse. This means that you can try a certain number of login attempts per second. If you exceed this limit, you will receive a `429 Too Many Requests` response. + +## Limited number of concurrent sessions + +The Pi-hole API only allows a limited number of concurrent sessions. This means that if you try to login with a new session while the maximum number of sessions is already active, you may be denied access. This is to prevent abuse and resource exhaustion. In case you hit this limit, please make sure to logout from your sessions when you don't need them anymore as this will free up API slots for future requests. Unused sessions will be automatically terminated after a certain amount of time. + +## Security Implications of Session-Based Authentication + +Session-based authentication, while convenient and widely used, does have several security implications that Pi-hole addresses in the following ways: + +1. **Session Hijacking**: If an attacker manages to steal a user's session ID, they can impersonate that user for the duration of the session. This is mitigated indirectly by the methods #2-#4 below and also by binding sessions to the client's IP address. Be aware that this may cause issues if your IP address changes during the session's lifetime (e.g., a device reconnecting to a different WiFi network). + +2. **Cross-Site Scripting (`XSS`)**: XSS attacks can be used to steal session IDs if they are stored in JavaScript-accessible cookies. Pi-hole's session cookie is not accessible to JavaScript (`HttpOnly`), so this is not a concern. + +3. **Cross-Site Request Forgery (`CSRF`)**: In a CSRF attack, an attacker tricks a victim into performing actions on their behalf. This is mitigated by using CSRF tokens or implementing the `SameSite` attribute for the session cookie. + +4. **Session Fixation**: In this attack, an attacker provides a victim with a session ID, and if the victim authenticates with that session ID, the attacker can use it to impersonate the victim. This is mitigated by ever reusing session IDs but always regenerating them for each new session. Pi-hole sources the session ID from a cryptographically secure random number generator to ensure that it is unique, unpredictable, and safe. + +5. **Idle Sessions**: If a session remains open for a long time without activity, it can be hijacked by an attacker. This is mitigated by both the high security of the generated SIDs, making them basically non-bruteforceable, and by the fact that the session ID is bound to the client's IP address. This means that an attacker would not only have to be on the same network as the victim but even be on the same machine to hijack their session. This is furthermore mitigated by implementing session timeouts. + +Remember, no security measure is foolproof, but by understanding the potential risks and the multiple layers of defense your Pi-hole implemented against these risks, you can make an informed decision about how to use the Pi-hole API securely in the context of your own scripts. Always use the secure transmission method (HTTPS) offered by your Pi-hole to access the API. The strong encryption will prevent attackers from eavesdropping on your requests and makes stealing your session ID basically impossible. + +{!abbreviations.md!} diff --git a/docs/api/index.md b/docs/api/index.md index 5cca32e..b84d41d 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -1,8 +1,16 @@ -# API Reference +# API Reference: Overview The Pi-hole API is organized around [REST](http://en.wikipedia.org/wiki/Representational_State_Transfer). Our API has predictable resource-oriented URLs, accepts and returns reliable UTF-8 [JavaScript Object Notation (JSON)-encoded](http://www.json.org/) data for all API responses, and uses standard HTTP response codes and verbs. -Most (but not all) endpoints require authentication. API endpoints requiring authentication will fail with code `401 Unauthorized` if no key is supplied. +Most (but not all) endpoints require authentication. API endpoints requiring authentication will fail with code `401 Unauthorized` if no key is supplied. See [API Reference: Authentication](auth.md) for details. + +## Accessing the API documentation + +The entire API is documented at http://pi.hole/api/docs and self-hosted by your Pi-hole to match 100% the API versions your local Pi-hole has. Using this locally served API documentation is preferred. In case you don't have Pi-hole installed yet, you can also check out the documentation for all branches online, e.g., [Pi-hole API documentation](https://ftl.pi-hole.net/development-v6/docs/) (branch `development-v6`). Similarly, you can check out the documentation for a specific other branches by replacing `development-v6` with the corresponding branch name. + +## API endpoints + +An overview of all available endpoints is available at the API documentation page. The endpoints are grouped by their functionality. ## JSON response @@ -28,9 +36,11 @@ The form of replies to successful requests strongly depends on the selected endp **Fields** Depending on the particular endpoint + -In contrast, errors have a uniform style to ease their programmatic treatment: +In contrast, errors have a uniform, predictable style to ease their programmatic treatment: + ???+ failure "Example reply: Error (unauthorized access)" Resource: `GET /api/domains` @@ -42,7 +52,7 @@ In contrast, errors have a uniform style to ease their programmatic treatment: "error": { "key": "unauthorized", "message": "Unauthorized", - "data": null + "hint": null } } ``` @@ -87,21 +97,21 @@ In contrast, errors have a uniform style to ease their programmatic treatment: Possible reason: The required field `domain` is missing in the payload - ??? info "Additional data (`"data": [object|null]`)" + ??? info "Additional data (`"hint": [object|string|null]`)" - The field `data` may contain a JSON object. Its content depends on the error itself and may contain further details such as the interpreted user data. If no additional data is available for this endpoint, `null` is returned instead of an object. + The field `hint` may contain a JSON object or string. Its content depends on the error itself and may contain further details such as the interpreted user data. If no additional hint is available for this endpoint, `null` is returned. - Examples for a failed request with `data` being set is (domain is already on this list): + Examples for a failed request with `hint` being set is (domain is already on this list): ``` json { "error": { - "key": "database_error", - "message": "Could not add to gravity database", - "data": { + "key": "database_error", + "message": "Could not add to gravity database", + "hint": { "argument": "abc.com", - "enabled": true, - "sql_msg": "UNIQUE constraint failed: domainlist.domain, domainlist.type" + "enabled": true, + "sql_msg": "UNIQUE constraint failed: domainlist.domain, domainlist.type" } } } @@ -116,13 +126,14 @@ Pi-hole's API uses the methods like this: Method | Description ---------|------------ -`GET` | **Read** from resource. The resource may not exist. -`POST` | **Create** resources -`PUT` | **Create or Replace** a resource. This method is commonly used to *update* entries. +`GET` | **Read** from resource +`POST` | **Create** a resource +`PATCH` | **Update** existing resource +`PUT` | **Create or Replace** a resource `DELETE` | **Delete** existing resource -??? info "Summarized details from [RFC 2616, Scn. 9](https://tools.ietf.org/html/rfc2616#section-9) (`GET/POST/PUT/DELETE`)" +??? info "Summarized details from [RFC 2616, Scn. 9](https://tools.ietf.org/html/rfc2616#section-9) (`GET/POST/PUT/DELETE`) and [RFC 2068, Scn. 19.6.1.1](https://datatracker.ietf.org/doc/html/rfc2068#section-19.6.1.1) (`PATCH`)" ### `GET` The `GET` method means retrieve whatever information (in the form of an entity) that is identified by the URI. @@ -146,6 +157,10 @@ Method | Description Use `PUT` primarily to **update existing records** (if the resource does not exist, the API will typically create a new record for it). If a new record has been added at the given URI, or an existing resource is modified, either the `200 (OK)` or `204 (No Content)` response codes are sent to indicate successful completion of the request. + ### `PATCH` + + The `PATCH` method is similar to `PUT` except that the entity contains a list of differences between the original version of the resource identified by the Request-URI and the desired content of the resource after the `PATCH`` action has been applied. The list of differences is in a format defined by the media type of the entity (e.g., "application/diff") and MUST include sufficient information to allow the server to recreate the changes necessary to convert the original version of the resource to the desired version. + ### `DELETE` As the name applies, `DELETE` APIs are used to **delete records** (identified by the Request-URI). diff --git a/mkdocs.yml b/mkdocs.yml index e9c8716..3332f04 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -123,6 +123,7 @@ nav: - 'Database recovery': database/gravity/recovery.md - 'Pi-hole API': - 'Overview': api/index.md + - 'Authentication': api/auth.md - 'FTLDNS': - 'Overview': ftldns/index.md - 'Configuration': ftldns/configfile.md