Skip to content

Commit

Permalink
Password protection via HTTP basic authentication (#34)
Browse files Browse the repository at this point in the history
* feat: add basic auth on /metrics route

* feat: add basic auth image & new prometheus config sample

* doc: add basic auth info & fix some style

* lint: black format

* chore: remove unused import
  • Loading branch information
italux authored Oct 16, 2021
1 parent 6f4f87c commit 5818780
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 22 deletions.
68 changes: 47 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@
+ [Run](#run)
+ [Docker](#docker)
+ [Samples](#samples)
* [Important Notes](#important-notes)
+ [Limitations](#limitations)
+ [Sentry API retry calls](#sentry-api-retry-calls)
+ [Recomendations & Tips](#recomendations--tips)
* [Documentation](#-documentation)
* [Contributing](#-contributing)
* [License](#-license)
* [Metrics](#metrics)
+ [Metrics Configuration](#metric-configuration)
+ [Basic Authentication](#basic-authentication)
* [Limitations](#limitations)
* [Recomendations & Tips](#recomendations--tips)
* [Documentation](#documentation)
* [Contributing](#contributing)
* [License](#license)
* [Show your support](#show-your-support)
* [Author](#author)

Expand Down Expand Up @@ -90,6 +91,35 @@ export SENTRY_ISSUES_14D=False
```
As with `SENTRY_AUTH_TOKEN`, all of these variables can be passed in through the `docker run -e VAR_NAME=<>` command or via the `.env` file if using Docker Compose.

### Basic Authentication

Security is always a concern and metrics can contain sensitive information that you'd like to protect and [HTTP Basic authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) is the most simple standard that uses fields in the HTTP header to restrict access to a specific URL.

To enable the basic authentication mechanism on Sentry Prometheus Exporter you just need to export the `SENTRY_EXPORTER_BASIC_AUTH` and configure the credentials as follows

#### Credentials configurations

| Environment variable | Value type | Default value | Purpose |
|:----------------------------------:|:----------:|:-------------:|:-------------------------------------------------------:|
| `SENTRY_EXPORTER_BASIC_AUTH` | Integer | prometheus | How many retries should be made in case of an exception |
| `SENTRY_EXPORTER_BASIC_AUTH_USER` | Integer | prometheus | How many retries should be made in case of an exception |
| `SENTRY_EXPORTER_BASIC_AUTH_PASS` | Float | prometheus | How many seconds to wait between retries |


Once enable, the `/metrics/` page will prompt `username` & `password` window

<img src="samples/basic_auth.png" width="500">

#### Prometheus configuration

If you enable the exporter HTTP basic authentication you'l need to configure prometheus scrape to pass the username & password defined on every scrape, please check prometheus [`<scrape_config>`](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) for more information.

```
basic_auth:
[ username: <string> ]
[ password: <secret> ]
```

## Samples

**Grafana Dashboard**
Expand All @@ -106,24 +136,20 @@ scrape_configs:
scrape_timeout: 4m
```
## ℹ️ Important Notes
### Limitations
## Limitations
- **Performance**: The exporter is serial, if your organization has a high number of issues & events you may experience `Context Deadline Exceeded` error during a Prometheus scrape

### Sentry API retry calls

The Sentry API limits the rate of requests to 3 per second, so the exporter retries on an HTTP exception.
- **Sentry API retry calls**: The Sentry API limits the rate of requests to 3 per second, so the exporter retries on an HTTP exception.

You can tweak retry settings with environment variables, though default settings should work:
You can tweak retry settings with environment variables, though default settings should work:

| Environment variable | Value type | Default value | Purpose |
|:------------------------:|:----------:|:-------------:|:-------------------------------------------------------:|
| `SENTRY_RETRY_TRIES` | Integer | 3 | How many retries should be made in case of an exception |
| `SENTRY_RETRY_DELAY` | Float | 1 | How many seconds to wait between retries |
| `SENTRY_RETRY_MAX_DELAY` | Float | 10 | Max delay to wait between retries |
| `SENTRY_RETRY_BACKOFF` | Float | 2 | Multiplier applied to delay between attempts |
| `SENTRY_RETRY_JITTER` | Float | 0.5 | Extra seconds added to delay between attempts |
| Environment variable | Value type | Default value | Purpose |
|:------------------------:|:----------:|:-------------:|:-------------------------------------------------------:|
| `SENTRY_RETRY_TRIES` | Integer | 3 | How many retries should be made in case of an exception |
| `SENTRY_RETRY_DELAY` | Float | 1 | How many seconds to wait between retries |
| `SENTRY_RETRY_MAX_DELAY` | Float | 10 | Max delay to wait between retries |
| `SENTRY_RETRY_BACKOFF` | Float | 2 | Multiplier applied to delay between attempts |
| `SENTRY_RETRY_JITTER` | Float | 0.5 | Extra seconds added to delay between attempts |

### Recomendations & Tips
- Use `scrape_interval: 5m` minimum.
Expand Down
49 changes: 48 additions & 1 deletion exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
from wsgiref.simple_server import make_server

from flask import Flask
from flask_httpauth import HTTPBasicAuth
from prometheus_client import make_wsgi_app, start_http_server
from prometheus_client.core import REGISTRY
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from werkzeug.serving import run_simple
from werkzeug.security import generate_password_hash, check_password_hash

from helpers.prometheus import SentryCollector, clean_registry
from libs.sentry import SentryAPI
Expand All @@ -17,6 +19,9 @@
AUTH_TOKEN = getenv("SENTRY_AUTH_TOKEN")
ORG_SLUG = getenv("SENTRY_EXPORTER_ORG")
PROJECTS_SLUG = getenv("SENTRY_EXPORTER_PROJECTS")
EXPORTER_BASIC_AUTH = getenv("SENTRY_EXPORTER_BASIC_AUTH") or "False"
EXPORTER_BASIC_AUTH_USER = getenv("SENTRY_EXPORTER_BASIC_AUTH_USER") or "prometheus"
EXPORTER_BASIC_AUTH_PASS = getenv("SENTRY_EXPORTER_BASIC_AUTH_PASS") or "prometheus"
LOG_LEVEL = getenv("LOG_LEVEL", "INFO")

log = logging.getLogger("exporter")
Expand All @@ -26,7 +31,28 @@
format="%(asctime)s - %(process)d - %(levelname)s - %(name)s - %(message)s",
)


app = Flask(__name__)
auth = HTTPBasicAuth()

users = {
EXPORTER_BASIC_AUTH_USER: generate_password_hash(EXPORTER_BASIC_AUTH_PASS),
}


def basic_auth_is_enabled(enabled):
"""Convert the human readable boolean to @auth.login_required optional param"""
if enabled == "True":
return False
else:
return True


@auth.verify_password
def verify_password(username, password):
"""verify that the username and password combination provided by the client are valid"""
if username in users and check_password_hash(users.get(username), password):
return username


def get_metric_config():
Expand All @@ -49,13 +75,14 @@ def get_metric_config():


@app.route("/")
def hello_world():
def home():
return "<h1>Sentry Issues & Events Exporter</h1>\
<h3>Go to <a href=/metrics/>/metrics</a></h3>\
"


@app.route("/metrics/")
@auth.login_required(optional=basic_auth_is_enabled(EXPORTER_BASIC_AUTH))
def sentry_exporter():
sentry = SentryAPI(BASE_URL, AUTH_TOKEN)
log.info("exporter: cleaning registry collectors...")
Expand All @@ -72,6 +99,26 @@ def sentry_exporter():
exit(1)

log.info("Starting simple wsgi server...")
log.info("auth: basic authentication enabled: {}".format(EXPORTER_BASIC_AUTH))

if (
EXPORTER_BASIC_AUTH == "True"
and EXPORTER_BASIC_AUTH_USER == "prometheus"
or EXPORTER_BASIC_AUTH_PASS == "prometheus"
):
log.info("auth: using default username and password.")
else:
log.debug(
'auth: using custom username: "{user}" and password: "{pwd}*******"'.format(
user=EXPORTER_BASIC_AUTH_USER,
pwd=EXPORTER_BASIC_AUTH_PASS[
# showing only half of the password characters to help debug
: len(EXPORTER_BASIC_AUTH_PASS)
- int(len(EXPORTER_BASIC_AUTH_PASS) / 2)
],
)
)

# The binding port was picked from the Default port allocations documentation:
# https://github.com/prometheus/prometheus/wiki/Default-port-allocations
run_simple(hostname="0.0.0.0", port=9790, application=app.wsgi_app)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Flask==2.0.2
Flask-HTTPAuth==4.4.0
prometheus-client==0.11.0
requests==2.26.0
retry==0.9.2
Binary file added samples/basic_auth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions samples/prometheus-basic-auth.yml.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
# my global config
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'sentry_exporter'

# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.

static_configs:
- targets: ['sentry-exporter:9790']
scrape_interval: 5m
scrape_timeout: 4m
basic_auth:
username: "prometheus"
password: "prometheus"

0 comments on commit 5818780

Please sign in to comment.