Skip to content

Commit 1993afb

Browse files
committed
Add Bearer authentication to /metrics endpoint
Adjust the Grafana examples to include the client error metric and add a variable to select the instance
1 parent 333879b commit 1993afb

15 files changed

+1540
-1211
lines changed

.dockerignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ conf/
2121
log_merging_config.json
2222
dockerComposeMPI.yml
2323
*.csv
24+
reversim-conf
2425

2526
# Debugpy logs, WinMerge Backups etc.
2627
*.log
@@ -29,6 +30,11 @@ dockerComposeMPI.yml
2930
# Ignore statistics folder
3031
statistics/
3132

33+
# Hide deployment storage
34+
tmp/
35+
secrets/
36+
37+
3238
# Maybe ignore unnecessary code
3339
# app/statistics
3440
# app/tests

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ __pycache__/
2121
env/
2222
venv/
2323
.venv/
24-
tmp/
2524

2625
# --- Debugpy logs, WinMerge Backups etc.
2726
*.log
@@ -44,3 +43,7 @@ reversim-conf
4443

4544
# --- Generated level thumbnails
4645
doc/levels
46+
47+
# --- Don't push instance relevant configuration
48+
tmp/
49+
secrets/

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
"hreserver",
144144
"hrestudy",
145145
"htmlsafe",
146+
"httpauth",
146147
"iframe",
147148
"imgdata",
148149
"imgstring",

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ ARG PROMETHEUS_MULTIPROC_DIR="/tmp/prometheus_multiproc"
1818
MAINTAINER Max Planck Institute for Security and Privacy
1919
LABEL org.opencontainers.image.authors="Max Planck Institute for Security and Privacy"
2020
# NOTE Also change the version in config.py
21-
LABEL org.opencontainers.image.version="2.1.0"
21+
LABEL org.opencontainers.image.version="2.1.1"
2222
LABEL org.opencontainers.image.licenses="AGPL-3.0-only"
2323
LABEL org.opencontainers.image.description="Ready to deploy Docker container to use ReverSim for research. ReverSim is an open-source environment for the browser, originally developed at the Max Planck Institute for Security and Privacy (MPI-SP) to study human aspects in hardware reverse engineering."
2424
LABEL org.opencontainers.image.source="https://github.com/emsec/ReverSim"
@@ -62,6 +62,7 @@ ENV PROMETHEUS_MULTIPROC_DIR=${PROMETHEUS_MULTIPROC_DIR}
6262
# Create empty statistics folders
6363
WORKDIR /usr/var/reversim-instance/statistics/LogFiles
6464
WORKDIR /usr/var/reversim-instance/statistics/canvasPics
65+
WORKDIR /usr/var/reversim-instance/secrets
6566
WORKDIR /usr/src/hregame
6667

6768
# Specify mount points for the statistics folder, levels, researchInfo & disclaimer

app/authentication.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import logging
2+
import os
3+
import secrets
4+
5+
from flask_httpauth import HTTPTokenAuth # type: ignore
6+
7+
from app.config import BEARER_TOKEN_BYTES
8+
from app.model.ApiKey import ApiKey
9+
from app.storage.database import db
10+
from app.utilsGame import safe_join
11+
12+
auth = HTTPTokenAuth(scheme='Bearer')
13+
14+
USER_METRICS = 'api_metrics'
15+
16+
@auth.verify_token # type: ignore
17+
def verifyToken(token: str) -> ApiKey|None:
18+
"""Check if this token exists. If yes return the user object, otherwise return `None`
19+
20+
https://flask-httpauth.readthedocs.io/en/latest/#flask_httpauth.HTTPTokenAuth.verify_token
21+
"""
22+
23+
return db.session.query(ApiKey).filter_by(token=token).first()
24+
25+
26+
def populate_data(instance_path: str):
27+
if ApiKey.query.count() < 1:
28+
apiKey = ApiKey(secrets.token_urlsafe(BEARER_TOKEN_BYTES), USER_METRICS)
29+
db.session.add(apiKey)
30+
db.session.commit()
31+
32+
defaultToken = db.session.query(ApiKey).where(ApiKey.user == USER_METRICS).first()
33+
if defaultToken is not None:
34+
35+
# Try to write the bearer secret to a file so other containers can use it
36+
try:
37+
folder = safe_join(instance_path, 'secrets')
38+
os.makedirs(folder, exist_ok=True)
39+
with open(safe_join(folder, 'bearer_api.txt'), encoding='UTF-8', mode='wt') as f:
40+
f.write(defaultToken.token)
41+
42+
except Exception as e:
43+
# When the file can't be created print the bearer to stdout
44+
logging.error('Could not write bearer token to file: ' + str(e))
45+
logging.info('Bearer token for /metrics endpoint: ' + defaultToken.token)

app/config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616

1717
# CONFIG Current Log File Version.
1818
# NOTE Also change this in the Dockerfile
19-
LOGFILE_VERSION = "2.1.0" # Major.Milestone.Subversion
19+
LOGFILE_VERSION = "2.1.1" # Major.Milestone.Subversion
2020

2121
PSEUDONYM_LENGTH = 32
2222
LEVEL_ENCODING = 'UTF-8' # was Windows-1252
2323
TIME_DRIFT_THRESHOLD = 200 # ms
2424
STALE_LOGFILE_TIME = 48 * 60 * 60 # close logfiles after 48h
2525
MAX_ERROR_LOGS_PER_PLAYER = 25
2626

27+
# The bearer token for the /metrics endpoint
28+
BEARER_TOKEN_BYTES = 32
29+
2730
# Number of seconds, after which the player is considered disconnected. A "Back Online"
2831
# message will be printed to the log, if the player connects afterwards. Also used for the
2932
# Prometheus Online Player Count metric

app/model/ApiKey.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from datetime import datetime, timezone
2+
from sqlalchemy import DateTime, String
3+
from sqlalchemy.orm import Mapped, mapped_column
4+
5+
from app.storage.database import db
6+
7+
8+
class ApiKey(db.Model):
9+
token: Mapped[str] = mapped_column(primary_key=True)
10+
user: Mapped[str] = mapped_column(String(64))
11+
created: Mapped[datetime] = mapped_column(DateTime)
12+
13+
def __init__(self, token: str, user: str) -> None:
14+
self.token = token
15+
self.user = user
16+
self.created = datetime.now(timezone.utc)

app/prometheusMetrics.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
from threading import Thread
33
import time
4+
from typing import Any
45
from flask import Flask
56

67
# Prometheus Metrics
@@ -14,27 +15,29 @@
1415

1516
class ServerMetrics:
1617
@staticmethod
17-
def __prometheusFactory():
18+
def __prometheusFactory(auth_provider: Any):
1819
EXCLUDED_PATHS = ["/?res\\/.*", "/?src\\/.*", "/?doc\\/.*"]
1920

2021
# Try to use the uWSGI exporter. This will fail, if uWSGI is not installed
2122
try:
2223
metrics = UWsgiPrometheusMetrics.for_app_factory( # type: ignore
23-
excluded_paths=EXCLUDED_PATHS
24+
excluded_paths=EXCLUDED_PATHS,
25+
metrics_decorator=auth_provider
2426
)
2527

2628
# Use the regular Prometheus exporter
2729
except Exception as e:
2830
logging.error(e)
2931

3032
metrics = PrometheusMetrics.for_app_factory( # type: ignore
31-
excluded_paths=EXCLUDED_PATHS
33+
excluded_paths=EXCLUDED_PATHS,
34+
metrics_decorator=auth_provider
3235
)
3336

3437
logging.info(f'Using {type(metrics).__name__} as the Prometheus exporter')
3538
return metrics
3639

37-
metrics = __prometheusFactory()
40+
metrics: PrometheusMetrics|UWsgiPrometheusMetrics|None = None
3841

3942
# ReverSim Prometheus Metrics
4043
#met_openLogs = Gauge("reversim_logfile_count", "The number of open logfiles") # type: ignore
@@ -47,8 +50,9 @@ def __prometheusFactory():
4750
met_clientErrors: Gauge|None = None
4851

4952
@classmethod
50-
def createPrometheus(cls, app: Flask):
53+
def createPrometheus(cls, app: Flask, auth_provider: Any):
5154
"""Init Prometheus"""
55+
cls.metrics = cls.__prometheusFactory(auth_provider)
5256
cls.metrics.init_app(app) # type: ignore
5357
cls.metrics.info('app_info', 'Application info', version=gameConfig.LOGFILE_VERSION) # type: ignore
5458

docker-compose-build.yml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@ volumes:
1010
# docker volume create reversim_playerdata
1111
playerdata:
1212
name: reversim_playerdata
13-
external: true
14-
15-
# Holds Flask, uWSGI and the game-config and all assets that are needed by the game
16-
gameConfig:
13+
#external: true
1714

1815
# If enabled in reversim_uwsgi.ini, uWSGI will log the server logs into text files
1916
# instead of stdout. This way you do not loose the logs when you restart
2017
# the Docker container
2118
#uwsgi_logs:
2219

20+
metric_secret:
21+
2322
# All containers that belong to this compose file
2423
services:
2524
# the container will be named reversim_game when launched
@@ -35,7 +34,7 @@ services:
3534
# mount your volumes here
3635
volumes:
3736
- playerdata:/usr/var/reversim-instance/statistics
38-
- gameConfig:/usr/var/reversim-instance/conf:ro
37+
- metric_secret:/usr/var/reversim-instance/secrets
3938
#- uwsgi_logs:/var/log/uwsgi
4039

4140
# add resource limits. You may want to tweak this for your deployment
@@ -82,6 +81,8 @@ services:
8281
uid: "65534"
8382
profiles:
8483
- grafana
84+
volumes:
85+
- metric_secret:/etc/reversim/secrets:ro
8586
#entrypoint: ["ls", "-lan", "/etc/prometheus"]
8687

8788
grafana:
@@ -108,9 +109,10 @@ configs:
108109
evaluation_interval: 15s # Evaluate rules every 15 seconds.
109110
110111
scrape_configs:
111-
- job_name: 'prometheus'
112+
- job_name: 'reversim'
112113
static_configs:
113114
- targets: ['game:8000']
115+
bearer_token_file: '/etc/reversim/secrets/bearer_api.txt'
114116
115117
grafana_datasources.yml:
116118
content: |

docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ services:
3434
# mount your volumes here
3535
volumes:
3636
- playerdata:/usr/var/reversim-instance/statistics
37-
- gameConfig:/usr/var/reversim-instance/conf:ro
3837
#- uwsgi_logs:/var/log/uwsgi
3938

4039
# add resource limits. You may want to tweak this for your deployment

0 commit comments

Comments
 (0)