From 3d786303021b9ae111bd27f2d5c26b3972bdbb3c Mon Sep 17 00:00:00 2001 From: Kareza Chen Date: Tue, 7 Nov 2023 00:58:37 +0800 Subject: [PATCH] support HTTPS/TLS. (#946) * support HTTPS/TLS. * Handling Andy's review comments 1.A better description of what's new. 2.Modify the parameter order of the start_wsgi_server method to maintain forward compatibility. 3.Enable authentication mode when safe. * revert tls_auth_handler func. * use "None" indicate "not provided". * TLS: Improved error handling; Documented capath * Update parameter names to make clear they are used to verify client certificates. * - Rename the `insecure_skip_verify` parameter to `client_auth_required`. - Update `_get_ssl_ctx` func, default not to set `ssl_ctx.verify_mode=ssl.CERT_REQUIRED`. - Update the description of default in README.md. Signed-off-by: kareza --------- Signed-off-by: kareza Co-authored-by: Andreas Maier --- README.md | 27 +++++++++++++-- prometheus_client/exposition.py | 58 +++++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ef9930a9..3aef0c4d 100644 --- a/README.md +++ b/README.md @@ -288,8 +288,8 @@ ProcessCollector(namespace='mydaemon', pid=lambda: open('/var/run/daemon.pid').r ### Platform Collector The client also automatically exports some metadata about Python. If using Jython, -metadata about the JVM in use is also included. This information is available as -labels on the `python_info` metric. The value of the metric is 1, since it is the +metadata about the JVM in use is also included. This information is available as +labels on the `python_info` metric. The value of the metric is 1, since it is the labels that carry information. ### Disabling Default Collector metrics @@ -327,6 +327,27 @@ To add Prometheus exposition to an existing HTTP server, see the `MetricsHandler which provides a `BaseHTTPRequestHandler`. It also serves as a simple example of how to write a custom endpoint. +By default, the prometheus client will accept only HTTP requests from Prometheus. +To enable HTTPS, `certfile` and `keyfile` need to be provided. The certificate is +presented to Prometheus as a server certificate during the TLS handshake, and +the private key in the key file must belong to the public key in the certificate. + +When HTTPS is enabled, you can enable mutual TLS (mTLS) by setting `client_auth_required=True` +(i.e. Prometheus is required to present a client certificate during TLS handshake) and the +client certificate including its hostname is validated against the CA certificate chain. + +`client_cafile` can be used to specify a certificate file containing a CA certificate +chain that is used to validate the client certificate. `client_capath` can be used to +specify a certificate directory containing a CA certificate chain that is used to +validate the client certificate. If neither of them is provided, a default CA certificate +chain is used (see Python [ssl.SSLContext.load_default_certs()](https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_default_certs)) + +```python +from prometheus_client import start_http_server + +start_http_server(8000, certfile="server.crt", keyfile="server.key") +``` + #### Twisted To use prometheus with [twisted](https://twistedmatrix.com/), there is `MetricsResource` which exposes metrics as a twisted resource. @@ -393,7 +414,7 @@ Such an application can be useful when integrating Prometheus metrics with ASGI apps. By default, the WSGI application will respect `Accept-Encoding:gzip` headers used by Prometheus -and compress the response if such a header is present. This behaviour can be disabled by passing +and compress the response if such a header is present. This behaviour can be disabled by passing `disable_compression=True` when creating the app, like this: ```python diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 13af927b..f2b7442b 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -159,7 +159,58 @@ def _get_best_family(address, port): return family, sockaddr[0] -def start_wsgi_server(port: int, addr: str = '0.0.0.0', registry: CollectorRegistry = REGISTRY) -> None: +def _get_ssl_ctx( + certfile: str, + keyfile: str, + protocol: int, + cafile: Optional[str] = None, + capath: Optional[str] = None, + client_auth_required: bool = False, +) -> ssl.SSLContext: + """Load context supports SSL.""" + ssl_cxt = ssl.SSLContext(protocol=protocol) + + if cafile is not None or capath is not None: + try: + ssl_cxt.load_verify_locations(cafile, capath) + except IOError as exc: + exc_type = type(exc) + msg = str(exc) + raise exc_type(f"Cannot load CA certificate chain from file " + f"{cafile!r} or directory {capath!r}: {msg}") + else: + try: + ssl_cxt.load_default_certs(purpose=ssl.Purpose.CLIENT_AUTH) + except IOError as exc: + exc_type = type(exc) + msg = str(exc) + raise exc_type(f"Cannot load default CA certificate chain: {msg}") + + if client_auth_required: + ssl_cxt.verify_mode = ssl.CERT_REQUIRED + + try: + ssl_cxt.load_cert_chain(certfile=certfile, keyfile=keyfile) + except IOError as exc: + exc_type = type(exc) + msg = str(exc) + raise exc_type(f"Cannot load server certificate file {certfile!r} or " + f"its private key file {keyfile!r}: {msg}") + + return ssl_cxt + + +def start_wsgi_server( + port: int, + addr: str = '0.0.0.0', + registry: CollectorRegistry = REGISTRY, + certfile: Optional[str] = None, + keyfile: Optional[str] = None, + client_cafile: Optional[str] = None, + client_capath: Optional[str] = None, + protocol: int = ssl.PROTOCOL_TLS_SERVER, + client_auth_required: bool = False, +) -> None: """Starts a WSGI server for prometheus metrics as a daemon thread.""" class TmpServer(ThreadingWSGIServer): @@ -168,6 +219,9 @@ class TmpServer(ThreadingWSGIServer): TmpServer.address_family, addr = _get_best_family(addr, port) 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) + httpd.socket = context.wrap_socket(httpd.socket, server_side=True) t = threading.Thread(target=httpd.serve_forever) t.daemon = True t.start() @@ -407,7 +461,7 @@ def tls_auth_handler( The default protocol (ssl.PROTOCOL_TLS_CLIENT) will also enable ssl.CERT_REQUIRED and SSLContext.check_hostname by default. This can be disabled by setting insecure_skip_verify to True. - + Both this handler and the TLS feature on pushgateay are experimental.""" context = ssl.SSLContext(protocol=protocol) if cafile is not None: