Skip to content

Commit

Permalink
support HTTPS/TLS. (#946)
Browse files Browse the repository at this point in the history
* 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 <kareza@qq.com>

---------

Signed-off-by: kareza <kareza@qq.com>
Co-authored-by: Andreas Maier <maiera@de.ibm.com>
  • Loading branch information
ethanschen and andy-maier committed Nov 6, 2023
1 parent a000193 commit 3d78630
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 5 deletions.
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
58 changes: 56 additions & 2 deletions prometheus_client/exposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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()
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 3d78630

Please sign in to comment.