diff --git a/README.md b/README.md index cfc5651..4a1769c 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,9 @@ You can use Prometheus to track various metrics about your Listory deployment. The metrics will be exposed on the `/api/metrics` endpoint. Make sure that this endpoint is not publicly available in your deployment. - `PROMETHEUS_ENABLED`: **false**, Set to `true` to enable Prometheus Metrics. +- `PROMETHEUS_BASIC_AUTH`: **false**, Set to `true` to require basic auth to access the metrics endpoint. +- `PROMETHEUS_BASIC_AUTH_USERNAME`: _Required_, if `PROMETHEUS_BASIC_AUTH` is `true`. +- `PROMETHEUS_BASIC_AUTH_PASSWORD`: _Required_, if `PROMETHEUS_BASIC_AUTH` is `true`. ## Development diff --git a/charts/listory/templates/deployment.yaml b/charts/listory/templates/deployment.yaml index 4846aab..695ceae 100644 --- a/charts/listory/templates/deployment.yaml +++ b/charts/listory/templates/deployment.yaml @@ -64,6 +64,21 @@ spec: {{- if .Values.prometheus.enabled }} - name: PROMETHEUS_ENABLED value: "true" + + {{- if .Values.prometheus.basicAuth.enabled }} + - name: PROMETHEUS_BASIC_AUTH + value: "true" + - name: PROMETHEUS_BASIC_AUTH_USERNAME + valueFrom: + secretKeyRef: + name: {{ include "listory.fullname" . }} + key: prometheus-basic-auth-username + - name: PROMETHEUS_BASIC_AUTH_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "listory.fullname" . }} + key: prometheus-basic-auth-password + {{- end }} {{- end }} securityContext: diff --git a/charts/listory/templates/secrets.yaml b/charts/listory/templates/secrets.yaml index 2858c1f..19b9fd6 100644 --- a/charts/listory/templates/secrets.yaml +++ b/charts/listory/templates/secrets.yaml @@ -8,3 +8,8 @@ type: Opaque data: spotify-client-secret: {{ .Values.spotify.clientSecret | b64enc | quote }} jwt-secret: {{ .Values.auth.jwtSecret | b64enc | quote }} + + {{- if .Values.prometheus.basicAuth.enabled }} + prometheus-basic-auth-username: {{ .Values.prometheus.basicAuth.username | b64enc | quote }} + prometheus-basic-auth-password: {{ .Values.prometheus.basicAuth.password | b64enc | quote }} + {{- end }} \ No newline at end of file diff --git a/charts/listory/values.yaml b/charts/listory/values.yaml index 6b949fe..fe16650 100644 --- a/charts/listory/values.yaml +++ b/charts/listory/values.yaml @@ -89,3 +89,8 @@ sentry: prometheus: enabled: false + + basicAuth: + enabled: false + username: "" + password: "" diff --git a/src/config/config.module.ts b/src/config/config.module.ts index 81f4a25..d3e2cca 100644 --- a/src/config/config.module.ts +++ b/src/config/config.module.ts @@ -45,6 +45,21 @@ import { ConfigModule as NestConfigModule } from "@nestjs/config"; // Prometheus for Metrics (Optional) PROMETHEUS_ENABLED: Joi.boolean().default(false), + PROMETHEUS_BASIC_AUTH: Joi.boolean().default(false), + PROMETHEUS_BASIC_AUTH_USERNAME: Joi.string().when( + "PROMETHEUS_BASIC_AUTH", + { + is: Joi.valid(true), + then: Joi.required(), + } + ), + PROMETHEUS_BASIC_AUTH_PASSWORD: Joi.string().when( + "PROMETHEUS_BASIC_AUTH", + { + is: Joi.valid(true), + then: Joi.required(), + } + ), }), }), ], diff --git a/src/metrics/metrics-auth.middleware.ts b/src/metrics/metrics-auth.middleware.ts new file mode 100644 index 0000000..f0c63ec --- /dev/null +++ b/src/metrics/metrics-auth.middleware.ts @@ -0,0 +1,36 @@ +import { + Injectable, + NestMiddleware, + UnauthorizedException, +} from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { IncomingMessage } from "http"; + +@Injectable() +export class MetricsAuthMiddleware implements NestMiddleware { + private readonly expectedHeaderValue: string; + + constructor(config: ConfigService) { + const username = config.get("PROMETHEUS_BASIC_AUTH_USERNAME"); + const password = config.get("PROMETHEUS_BASIC_AUTH_PASSWORD"); + + this.expectedHeaderValue = MetricsAuthMiddleware.buildHeaderValue( + username, + password + ); + } + + private static buildHeaderValue(username: string, password: string): string { + return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`; + } + + use(req: IncomingMessage, res: any, next: () => void) { + const header = req.headers?.authorization; + + if (header !== this.expectedHeaderValue) { + throw new UnauthorizedException("MetricsBasicAuthNotMatching"); + } + + next(); + } +} diff --git a/src/metrics/metrics.module.ts b/src/metrics/metrics.module.ts index a89c5af..103d7a0 100644 --- a/src/metrics/metrics.module.ts +++ b/src/metrics/metrics.module.ts @@ -6,6 +6,8 @@ import { Module, NestModule, } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { MetricsAuthMiddleware } from "./metrics-auth.middleware"; // Dirty hack because we can not conditionally import modules based on // injected services and upstream module does not support dynamic configuration @@ -13,8 +15,12 @@ import { // https://github.com/digikare/nestjs-prom/issues/27 const promEnabled = process.env.PROMETHEUS_ENABLED === "true"; +const METRIC_PATH = "/api/metrics"; + @Module({}) export class MetricsModule implements NestModule { + constructor(private readonly config: ConfigService) {} + static forRoot(): DynamicModule { const module = { imports: [], @@ -22,7 +28,7 @@ export class MetricsModule implements NestModule { }; if (promEnabled) { const promOptions = { - metricPath: "/api/metrics", + metricPath: METRIC_PATH, withDefaultsMetrics: true, withDefaultController: true, }; @@ -42,10 +48,14 @@ export class MetricsModule implements NestModule { } configure(consumer: MiddlewareConsumer) { - if (promEnabled) { + if (this.config.get("PROMETHEUS_ENABLED")) { // We register the Middleware ourselves to avoid tracking // latency for static files served for the frontend. - consumer.apply(InboundMiddleware).forRoutes("/api"); + consumer.apply(InboundMiddleware).exclude(METRIC_PATH).forRoutes("/api"); + + if (this.config.get("PROMETHEUS_BASIC_AUTH")) { + consumer.apply(MetricsAuthMiddleware).forRoutes(METRIC_PATH); + } } } }