Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion prometheus_client/exposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ def _get_ssl_ctx(
cafile: Optional[str] = None,
capath: Optional[str] = None,
client_auth_required: bool = False,
tls_min_version: ssl.TLSVersion = ssl.TLSVersion.MINIMUM_SUPPORTED,
tls_max_version: ssl.TLSVersion = ssl.TLSVersion.MAXIMUM_SUPPORTED
) -> ssl.SSLContext:
"""Load context supports SSL."""
ssl_cxt = ssl.SSLContext(protocol=protocol)
Expand Down Expand Up @@ -227,6 +229,9 @@ def _get_ssl_ctx(
raise exc_type(f"Cannot load server certificate file {certfile!r} or "
f"its private key file {keyfile!r}: {msg}")

ssl_cxt.minimum_version = tls_min_version
ssl_cxt.maximum_version = tls_max_version

return ssl_cxt


Expand All @@ -240,6 +245,8 @@ def start_wsgi_server(
client_capath: Optional[str] = None,
protocol: int = ssl.PROTOCOL_TLS_SERVER,
client_auth_required: bool = False,
tls_min_version: ssl.TLSVersion = ssl.TLSVersion.MINIMUM_SUPPORTED,
tls_max_version: ssl.TLSVersion = ssl.TLSVersion.MAXIMUM_SUPPORTED
) -> Tuple[WSGIServer, threading.Thread]:
"""Starts a WSGI server for prometheus metrics as a daemon thread."""

Expand All @@ -250,7 +257,16 @@ class TmpServer(ThreadingWSGIServer):
app = make_wsgi_app(registry)
httpd = make_server(addr, port, app, TmpServer, handler_class=_SilentHandler)
if certfile and keyfile:
context = _get_ssl_ctx(certfile, keyfile, protocol, client_cafile, client_capath, client_auth_required)
context = _get_ssl_ctx(
certfile,
keyfile,
protocol,
client_cafile,
client_capath,
client_auth_required,
tls_min_version,
tls_max_version
)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
t = threading.Thread(target=httpd.serve_forever)
t.daemon = True
Expand Down
17 changes: 17 additions & 0 deletions tests/certs/client-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICrzCCAZcCFCVu7nbOAxRNKBYa2cl22rdRCtvfMA0GCSqGSIb3DQEBCwUAMBIx
EDAOBgNVBAMMB1Rlc3QgQ0EwHhcNMjYwNTI2MTQyMTU0WhcNMzYwNTIzMTQyMTU0
WjAWMRQwEgYDVQQDDAt0ZXN0LWNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAJM2/f+8BBKjAlSF/9eiuB2444A2g6V007U5shZhBuPC9cNDxGKM
W1WT3QsgvxOdagdaANkpufqHcYixgFhx/v3lSEzlzd3uXyFMOiK7BdiPsctkqlWZ
VGIuUPpWwvJHWS4R5V1nYNCVsgyZB9XGThl7IknQzBK+tkY2GepqPQXyx1/AP7aB
AlTVBx3r7jTWvkrzvAdrcevrjhOOJUbPmgoiiEGSQeZSMvkdLERujvu5Y3wno2Mg
vcHJxCJwZ5y0RakmTzyAZLHke9lMavgt9F5yEA8G/8SnnXy6HrUp6B6I8Z1eLnof
b3mjUwiGxqDwEVBQHfMtOH6uC7ZE6zbNB1cCAwEAATANBgkqhkiG9w0BAQsFAAOC
AQEAJBchyhT2iyg42qi3uUE1NeCcEb/gM82LeihZbDd38ItUdU7TFqk7wEwsUNJk
k1uwNFVlyMGbHD1IvCAS4L8l/9uPaDG4DmLZ42shFRCaABNEFlKtGPa+YNuhFJ5z
DZKaLaJp8BKpvmoH+iPmsoCDlADwWmLgbdeFBGnHRuOnJBSmEEjQFrnz3jKrX6Lk
+IxVX5Rdp9xOKHBJkj99mgseEYZQk2YFFBCzHX7NNl6wBk/usKJoJeaOPhl9eOGK
VaUOfEdO5NuTRf9nPOORzqFtW3ErNjNjPjKN8VppHtXhRO6dWsmzGnmjVChxoZWC
H0rRJtGcab5HWf94laJilCj7Cw==
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions tests/certs/client-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCTNv3/vAQSowJU
hf/XorgduOOANoOldNO1ObIWYQbjwvXDQ8RijFtVk90LIL8TnWoHWgDZKbn6h3GI
sYBYcf795UhM5c3d7l8hTDoiuwXYj7HLZKpVmVRiLlD6VsLyR1kuEeVdZ2DQlbIM
mQfVxk4ZeyJJ0MwSvrZGNhnqaj0F8sdfwD+2gQJU1Qcd6+401r5K87wHa3Hr644T
jiVGz5oKIohBkkHmUjL5HSxEbo77uWN8J6NjIL3BycQicGectEWpJk88gGSx5HvZ
TGr4LfRechAPBv/Ep518uh61KegeiPGdXi56H295o1MIhsag8BFQUB3zLTh+rgu2
ROs2zQdXAgMBAAECggEAATafUlJzkCRtelKJFiG+YGmr37HTVPOeY8HVe29noKH0
kkbxNpoPOKiEK7l53wiu8oo7M+RZpucjOEFfnEWtmIchbkIoomR6vpSubVHa+FAl
jYEcvEw2u1ZuuW7Uotg+s8KsVXWVgTKdVJLq/cfpezaeGjtRK0hiH+MF71OFLD2I
UoszlVbTI9FAP+xwuFSJO4xyOirz2VmqgYvQd+qTuuPU2ZjPHFbBUXm6JDpchGJk
WdPp/7qEWKFwDufvgkA5rCFxwsiReQ9HfOS2f4l+7eg2uyjXAClYTt/lYq9PK1Ut
sk/R1Gq5C4S8G0f04Jk8J2bQKS57oRALfaJEps5LUQKBgQDPCPID4w9TnAhWoHtR
L5ps02KLi52sw9F3EVedVX2BjMM/jvRwtzg8I8iaWAE1iL0t8lDQRxbUcgNyWRvi
0/WG/2IESVlciqd4XuITLthj1PDIpIM2iCjQZpZKDqe9bVRPx/AY+UNV1aLSCEbF
xGS+uYoQRGpmiSYnRaQzIzgn0QKBgQC2CDOD1/1sEVFbfsWJTaAM3YjsQ1I5mXFI
HhoWpMKBUogWBXp9dzO4Ae/iRo0QviVUUY2bHlJjCoaQ0FzuiieZIhbOwHG2Qtf3
JzmUaOSMecwsTeM05XHciwY+sWU/Udw7EzDhVpOHPZR31LKeapchUJGnofnOdkcY
zaHEwiuupwKBgQC6I0bD698Zws1UZRC6G1xxv1N4NtxaOewXawYktHoUgaQBftuS
g4gRufJfogPkR74ekx/JQkDqXF9w7WC+/OZgqzdKt0+afia3eEc2DAYNK6QYIKC/
5IcdZz5z8t0o2CTXXeEl8uVxRJQQ1dQbdslFGLdijMBE08XzxQ8t0tpoIQKBgH09
U0QovME3gQQ0SnBXKgDwAp6bCt16RshZfZWKshAL2nlcN5RPCRRWsNa7t56HVGOY
4JaS3BgsS70ivm2YO/pNy+df3FyLzM7M+/6x1F0aB3GL/QCNxDL6q8dCgeh4x88V
OxIuYL4xjg6MFoCL0YMoTa5J8PctxWi5Qc1/0lINAoGASyTZT8emfSDW0+kpqiYw
y+4ftFxqYPAVCf2IWGeQL8TrfkxUiJ7r4Pu5VK9nuYvMR/u4mvnJSG6F1NuJhxzY
4kUnoOnPhITLZjUvNE/xQEuhiJndiehZgSj0JAU6MqGa4pZOxZfqPAfhEF62b5wx
6Wlh7JxQAM+6agEfM3/OS3Y=
-----END PRIVATE KEY-----
19 changes: 19 additions & 0 deletions tests/certs/server-ca.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDBTCCAe2gAwIBAgIUMrjGc/qUt+rpFb14OvBFePSMQRIwDQYJKoZIhvcNAQEL
BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjA1MjYxNDIxNTNaFw0zNjA1MjMx
NDIxNTNaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQDFC2wPWOqoFHIYXASjL06yUnG0SlqLqw4oZphdj/q4pbyYPcva
QKI8m4u7Tq/l8JQmbd0sHqGMVYLmACX5ygkppzepz/bVgDeij7RztUgDjJwvUxAC
SEAss0dcE19P57j5ad24xmyV2iP0RK7oXnjapDrH1fhqvIyfybqRxt+50NODRh1t
z471240lDBPOG3ReRZ06dYEpzYaq3PQPatPJnaLGOmsf2NQ8sETTK35vcTMZrXsr
vzrftUCKn4DRyyZ58GE1VpevbVi8z/vHzWBYpRcHTZvfnOz12ijCd2wvnEtTu8TO
+GZS5j84KSF4AI7FlhDMPAS3/dhSLzXgnd4lAgMBAAGjUzBRMB0GA1UdDgQWBBRi
IztvE2ErRLmziv1XxxHCbism8DAfBgNVHSMEGDAWgBRiIztvE2ErRLmziv1XxxHC
bism8DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAjBA50Oadg
Fx6d/cxzHCEd29BluqxjfzYc4TAeTP4NSQPWKXM7BgIqZbHDS/IAK/Xfd1raVCm5
yv2v4Pe+0fTkezUk5uZjtgZoH5+o4aQL9GdbLO93F4rxzZhpoY92iaXAsoDEntRO
YyDnxLna6csiH4hyvr6Q8Yih/lDysw7DB1jozkFeZtX4ZsVFpsDnYLa5OQjJErpw
9GCM0NEzEW6HlqblsAuBv3DHavUAzfR4obD+Md60BRMxwC5Otl63sS99y81ycs2S
ffW0rLDtgB9hShCXBNeZkGsPrLwBr00nK7bvGaZwOU0Ysuxg+elP3cOXD2UcW7lc
Q+ZBinkQEFpB
-----END CERTIFICATE-----
19 changes: 19 additions & 0 deletions tests/certs/server-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDEjCCAfqgAwIBAgIUJW7uds4DFE0oFhrZyXbat1EK294wDQYJKoZIhvcNAQEL
BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjA1MjYxNDIxNTNaFw0zNjA1MjMx
NDIxNTNaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAOl3Gnz9y+OHr3MIetuX7a/Bvo/lYkwTnmQpT4YC1ycalM0+
M4wCewrbpn5RoNbH4j+oB6URuiLWlhk3SP9hOwbVbt4rOyCaulAVa5G0B46eqkG6
ZDi9EWkt+L7Zvfwe+MbdG25xmRumkvc6iv8b+/0mM3t5cm3E8BHpwPi6kfoFmFHn
qREDfI4406tBkbSYUkRQL8qZvSWMo5HgIYmgrkKiWuZNV4bNYKHUOyaOqWvgn8En
ZxrlGt2ezHif/SFz+EaYJZkjBDJ5rMbwxl1BAVZKpSubKt5U59zXLkuachZF5sAY
sEd/LoZxF23/qP5y16C4mVzBYO/0z2udNOW9YAcCAwEAAaNeMFwwGgYDVR0RBBMw
EYIJbG9jYWxob3N0hwR/AAABMB0GA1UdDgQWBBSUEOHINPcDxtBsfNF5xSTUSqfF
vDAfBgNVHSMEGDAWgBRiIztvE2ErRLmziv1XxxHCbism8DANBgkqhkiG9w0BAQsF
AAOCAQEALxRf0TSusmJXO9pj5t3Njxc6VS+Ts/MnmE1NTloCCkVMEfYYzqROWHME
LOCg2YSnqX6S6Gwk3zjBSuT7aA4SNQ3lD9HndRYa5k+6/6qunnz5Q/g205GJ97us
HqkvdDjLE7lGmM5pIVjoyeMOWiQ6+EOtMt0CmL0nfqJ0DsDUZHVB7NB+MW20EVmC
XiXr52SuvKHDIms3QFkZWOi+scOKleQnvEVU7VqrQamKNtf8fxxGa3/AvjLLJ1ra
q9eB590eajBDdg50FttYLwyA/yb6cqrfIMfrHRj3R//yE2avtUkrKN6FgCtqgpoa
ZsIk6qEmFQWUTglyLwhk0f6m21FKAA==
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions tests/certs/server-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDpdxp8/cvjh69z
CHrbl+2vwb6P5WJME55kKU+GAtcnGpTNPjOMAnsK26Z+UaDWx+I/qAelEboi1pYZ
N0j/YTsG1W7eKzsgmrpQFWuRtAeOnqpBumQ4vRFpLfi+2b38HvjG3RtucZkbppL3
Oor/G/v9JjN7eXJtxPAR6cD4upH6BZhR56kRA3yOONOrQZG0mFJEUC/Kmb0ljKOR
4CGJoK5ColrmTVeGzWCh1Dsmjqlr4J/BJ2ca5Rrdnsx4n/0hc/hGmCWZIwQyeazG
8MZdQQFWSqUrmyreVOfc1y5LmnIWRebAGLBHfy6GcRdt/6j+cteguJlcwWDv9M9r
nTTlvWAHAgMBAAECggEAJ2/ZlxyOGPC+J+naSwbWfTZ2mLsQSDaWLmg2CTaonm/k
i+kCbxeqLjLdZIAoca+RHdyl8fHVJfZmo3rNx2nmvShHkprt4XuRll6P7axiDGrr
6q9wJ490hfZgiuigKZsXvgvyis0ApoWUVNPcT+yru978mlJxDG7UeMoqMTne18NQ
jKw8zTPrcDnSqxdzNs9hGcM/RYDAnNM1jFqnmJpJF9nTMf9gDOYQMJCb7yUZVOj/
ccyc2AjTdp6corXZpSqiYHz4UwfZ0Wvf7BXwBAOxdYnM6qlZXc+RKZJYnlUbSNHZ
IRkIZXsRILAIgpL1fAhMAQodyFMjNU9wQAkva6j49QKBgQD8ohxWR3nKg9KXhSTU
7sv1E5WzUCEAX5gJTSRx9S8lAAotGEByaTO/pWcYtpo+jXukukERhDdnsxR7SJAf
7jOYLUQgqFgZXD/U7vaCNuYoRG0E1MwSqZnVIXOpZW2z6j0PwcbXLJVeLDpDctn1
Ga7VvYiv7/KuvRSQfkSOrH8USwKBgQDsk5lLSOQb7Ke53gfHjeGMU4646JcNDFnD
hWtXQujABwQZmSDWudCvsLWwDr0O0kUDqDcPCEMhbNYo388DwowzHnE35Tzmzo5D
R/YZ+Mh+UuW+e5gLxdmn7Z1xENKft/4ceOkeBBExQumZYYsaNVA7znzotaPSjRfH
J36QHsG1tQKBgQC9NMBaUf/CD4ZaWqpiG1J/gyJ8AEgnGnEojjD8dC/R2zzD10T1
KxtJrhwPozrUHGx8y83Ny6MfNDzjtE3UzDayAzzh5JLOs4tO84WFso4fnFe15ZXN
aF5BBGO2e7N0qrr+oRdFsitQM3mTaGIashiCFghYFDJCcnQDX74CyOgIDwKBgCht
JHHf99LpwtOZJF0uWo9/K9FfNYiuRpyJrQkRTvKZgFLbfugSgp2zJaj7K8Vfmxl/
4kC4WbhZf9MmQ5rR4OFPX2t8ycZrH5ZRsrVHdQNZKRc+yYGhgosWqKPMiyFt8Idv
Be7yJPn1BDQInhuRZq+BnoipmV/+akTG8/Kuvs1NAoGALLN5lPRdZdTvjoiougt9
MxqGfBR9H8PfAo/Eu8Et5Otln3P1Vl3SgeiwDGVb59avfBQ5N4UecTHMp/2jbxOw
w/AzvF9LMLtXKdyqnOeBfP2xgbEZ9chLeoePEYkATpQfgjs7qmzK+mwZin5EyjFa
tqn7AnX5AnDRtPIC10Z05rA=
-----END PRIVATE KEY-----
146 changes: 145 additions & 1 deletion tests/test_exposition.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import gzip
from http.server import BaseHTTPRequestHandler, HTTPServer
import os
import ssl
import threading
import time
import unittest
import urllib

import pytest

Expand All @@ -16,7 +18,7 @@
from prometheus_client.core import GaugeHistogramMetricFamily, Timestamp
from prometheus_client.exposition import (
basic_auth_handler, choose_encoder, default_handler, MetricsHandler,
passthrough_redirect_handler, tls_auth_handler,
passthrough_redirect_handler, start_wsgi_server, tls_auth_handler,
)
import prometheus_client.openmetrics.exposition as openmetrics

Expand Down Expand Up @@ -633,6 +635,148 @@ def test_prom_no_version(self):
self.assert_is_prom(exp)


class TestWsgiTLS(unittest.TestCase):
def setUp(self):
self.certs_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'certs'
)
self.httpd = None
self.t = None

def tearDown(self):
if self.httpd:
self.httpd.shutdown()
self.httpd.server_close()
self.t.join()

def _assert_tls_connection(
self,
server_kwargs,
use_server_tls=True,
client_tls_kwargs=None,
request_tls_version=ssl.TLSVersion.TLSv1_3,
expect_exception=None
):
self.httpd, self.t = start_wsgi_server(port=0, **server_kwargs)
port = self.httpd.server_address[1]

if use_server_tls:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.minimum_version = request_tls_version
ctx.maximum_version = request_tls_version
ctx.load_verify_locations(
os.path.join(self.certs_dir, "server-ca.pem")
)

if client_tls_kwargs is not None:
ctx.load_cert_chain(**client_tls_kwargs)

url = f"https://localhost:{port}/metrics"
else:
ctx = None
url = f"http://localhost:{port}/metrics"

if expect_exception is not None:
self.assertRaises(
expect_exception,
urllib.request.urlopen,
url,
context=ctx
)
else:
response = urllib.request.urlopen(url, context=ctx)
self.assertEqual(response.status, 200)

def test_tls_disabled(self):
self._assert_tls_connection(server_kwargs={}, use_server_tls=False)

def test_tls_enabled(self):
server_kwargs = {
"certfile": os.path.join(self.certs_dir, "server-cert.pem"),
"keyfile": os.path.join(self.certs_dir, "server-key.pem"),
}
self._assert_tls_connection(server_kwargs)

def test_tls_untrusted_server_cert_raises(self):
server_kwargs = {
"certfile": os.path.join(self.certs_dir, "cert.pem"),
"keyfile": os.path.join(self.certs_dir, "key.pem"),
}
self._assert_tls_connection(
server_kwargs,
expect_exception=urllib.error.URLError
)

def test_tls_versions_configured_correctly(self):
server_kwargs = {
"certfile": os.path.join(self.certs_dir, "server-cert.pem"),
"keyfile": os.path.join(self.certs_dir, "server-key.pem"),
"tls_min_version": ssl.TLSVersion.TLSv1_2,
"tls_max_version": ssl.TLSVersion.TLSv1_3,
}
self._assert_tls_connection(
server_kwargs,
request_tls_version=ssl.TLSVersion.TLSv1_2
)

def test_tls_using_lower_version_than_min_raises(self):
server_kwargs = {
"certfile": os.path.join(self.certs_dir, "server-cert.pem"),
"keyfile": os.path.join(self.certs_dir, "server-key.pem"),
"tls_min_version": ssl.TLSVersion.TLSv1_3,
}
self._assert_tls_connection(
server_kwargs,
request_tls_version=ssl.TLSVersion.TLSv1_2,
expect_exception=urllib.error.URLError
)

def test_tls_using_higher_version_than_max_raises(self):
server_kwargs = {
"certfile": os.path.join(self.certs_dir, "server-cert.pem"),
"keyfile": os.path.join(self.certs_dir, "server-key.pem"),
"tls_max_version": ssl.TLSVersion.TLSv1_2,
}
self._assert_tls_connection(
server_kwargs,
request_tls_version=ssl.TLSVersion.TLSv1_3,
expect_exception=urllib.error.URLError
)

def test_mtls_enabled(self):
server_kwargs = {
"certfile": os.path.join(self.certs_dir, "server-cert.pem"),
"keyfile": os.path.join(self.certs_dir, "server-key.pem"),
"client_auth_required": True,
"client_cafile": os.path.join(self.certs_dir, "server-ca.pem"),
}
client_tls_kwargs = {
"certfile": os.path.join(self.certs_dir, "client-cert.pem"),
"keyfile": os.path.join(self.certs_dir, "client-key.pem")
}
self._assert_tls_connection(
server_kwargs,
client_tls_kwargs=client_tls_kwargs
)

def test_mtls_untrusted_client_cert_raises(self):
server_kwargs = {
"certfile": os.path.join(self.certs_dir, "server-cert.pem"),
"keyfile": os.path.join(self.certs_dir, "server-key.pem"),
"client_auth_required": True,
"client_cafile": os.path.join(self.certs_dir, "server-cert.pem"),
}
client_tls_kwargs = {
"certfile": os.path.join(self.certs_dir, "cert.pem"),
"keyfile": os.path.join(self.certs_dir, "key.pem")
}
self._assert_tls_connection(
server_kwargs,
client_tls_kwargs=client_tls_kwargs,
expect_exception=ssl.SSLError
)


@pytest.mark.parametrize("scenario", [
{
"name": "empty string",
Expand Down