Add mTLS support for Loki monitoring hook #1293
5 changed files with 121 additions and 5 deletions
|
|
@ -3098,6 +3098,24 @@ properties:
|
|||
Grafana Loki log URL to notify when a backup begins,
|
||||
ends, or fails.
|
||||
example: "http://localhost:3100/loki/api/v1/push"
|
||||
tls:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
cert_path:
|
||||
type: string
|
||||
description: |
|
||||
Path to a PEM client certificate file for mutual
|
||||
TLS authentication.
|
||||
example: /etc/borgmatic/loki-client.crt
|
||||
key_path:
|
||||
type: string
|
||||
description: |
|
||||
Path to a PEM private key file for the client
|
||||
certificate.
|
||||
example: /etc/borgmatic/loki-client.key
|
||||
description: |
|
||||
TLS options for mutual TLS (mTLS) authentication with Loki.
|
||||
labels:
|
||||
type: object
|
||||
additionalProperties:
|
||||
|
|
|
|||
|
|
@ -27,9 +27,16 @@ class Loki_log_buffer:
|
|||
adding labels to the log stream and takes care of communication with Loki.
|
||||
'''
|
||||
|
||||
def __init__(self, url, dry_run):
|
||||
def __init__(self, url, dry_run, tls_cert_path=None, tls_key_path=None):
|
||||
'''
|
||||
Given a Loki URL, a dry run flag, and optional TLS certificate and key paths for mTLS authentication,
|
||||
create an instance of Loki_log_buffer.
|
||||
'''
|
||||
|
||||
self.url = url
|
||||
self.dry_run = dry_run
|
||||
self.tls_cert_path = tls_cert_path
|
||||
self.tls_key_path = tls_key_path
|
||||
self.root = {'streams': [{'stream': {}, 'values': []}]}
|
||||
|
||||
def add_value(self, value):
|
||||
|
|
@ -77,6 +84,7 @@ class Loki_log_buffer:
|
|||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'borgmatic',
|
||||
},
|
||||
cert=(self.tls_cert_path, self.tls_key_path) if self.tls_cert_path else None,
|
||||
)
|
||||
result.raise_for_status()
|
||||
except requests.RequestException:
|
||||
|
|
@ -88,7 +96,7 @@ class Loki_log_handler(logging.Handler):
|
|||
A log handler that sends logs to Loki.
|
||||
'''
|
||||
|
||||
def __init__(self, url, send_logs, log_level, dry_run):
|
||||
def __init__(self, url, send_logs, log_level, dry_run, tls_cert_path=None, tls_key_path=None):
|
||||
'''
|
||||
Given a URL to send logs to, whether all borgmatic logs should be sent (or just explicitly
|
||||
added messages from this hook), the log level to use (influencing which logs get sent), and
|
||||
|
|
@ -96,7 +104,9 @@ class Loki_log_handler(logging.Handler):
|
|||
'''
|
||||
super().__init__()
|
||||
|
||||
self.buffer = Loki_log_buffer(url, dry_run)
|
||||
self.buffer = Loki_log_buffer(
|
||||
url, dry_run, tls_cert_path=tls_cert_path, tls_key_path=tls_key_path
|
||||
)
|
||||
self.send_logs = send_logs
|
||||
self.setLevel(log_level)
|
||||
|
||||
|
|
@ -139,7 +149,24 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
|
|||
Add a handler to the root logger to regularly send the logs to Loki.
|
||||
'''
|
||||
url = hook_config.get('url')
|
||||
loki = Loki_log_handler(url, hook_config.get('send_logs', False), monitoring_log_level, dry_run)
|
||||
tls = hook_config.get('tls', {})
|
||||
|
||||
if bool(tls.get('cert_path')) != bool(tls.get('key_path')):
|
||||
logger.critical(
|
||||
'Invalid Loki TLS configuration: cert_path and key_path must both be set or both be unset'
|
||||
)
|
||||
raise ValueError(
|
||||
'Invalid Loki TLS configuration: cert_path and key_path must both be set or both be unset'
|
||||
)
|
||||
|
||||
loki = Loki_log_handler(
|
||||
url,
|
||||
hook_config.get('send_logs', False),
|
||||
monitoring_log_level,
|
||||
dry_run,
|
||||
tls_cert_path=tls.get('cert_path'),
|
||||
tls_key_path=tls.get('key_path'),
|
||||
)
|
||||
|
||||
for key, value in hook_config.get('labels').items():
|
||||
if value == '__hostname':
|
||||
|
|
|
|||
|
|
@ -89,3 +89,27 @@ for more information.
|
|||
<span class="minilink minilink-addedin">New in version 2.0.0</span>Set the
|
||||
defaults for these flags in your borgmatic configuration via the
|
||||
`monitoring_verbosity`, `list`, and `statistics` options.
|
||||
|
||||
|
||||
### Mutual TLS authentication
|
||||
|
||||
<span class="minilink minilink-addedin">New in version **TBD**</span> Since
|
||||
Loki does not come with a built-in authentication layer [(doc)](https://grafana.com/docs/loki/latest/operations/authentication/), this feature is typically used
|
||||
alongside a reverse proxy (such as [nginx](https://docs.nginx.com/waf/configure/secure-mtls/) or
|
||||
[Traefik](https://doc.traefik.io/traefik/reference/routing-configuration/http/tls/tls-options/#client-authentication-mtls)) that handles mTLS termination.
|
||||
If your setup is configured for mTLS authentication, you can provide a client certificate and private key:
|
||||
|
||||
```yaml
|
||||
loki:
|
||||
url: https://loki.fqdn/loki/api/v1/push
|
||||
|
||||
labels:
|
||||
app: borgmatic
|
||||
|
||||
tls:
|
||||
cert_path: /etc/borgmatic/loki-client.crt
|
||||
key_path: /etc/borgmatic/loki-client.key
|
||||
```
|
||||
|
||||
Both `cert_path` and `key_path` must be [PEM-encoded](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail). They are passed directly
|
||||
to the underlying HTTP client, so the standard mutual TLS handshake is performed for every request borgmatic sends to Loki.
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ def test_ping_monitor_sends_log_message():
|
|||
config_filename = 'test.yaml'
|
||||
post_called = False
|
||||
|
||||
def post(url, data, timeout, headers):
|
||||
def post(url, data, timeout, headers, **kwargs):
|
||||
nonlocal post_called
|
||||
post_called = True
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks.monitoring import loki as module
|
||||
|
|
@ -78,3 +79,49 @@ def test_loki_log_handler_flush_with_empty_buffer_does_not_raise():
|
|||
handler = module.Loki_log_handler(flexmock(), send_logs=False, log_level=10, dry_run=False)
|
||||
|
||||
handler.flush()
|
||||
|
||||
|
||||
def test_loki_log_buffer_init_with_tls_stores_cert_and_key_paths():
|
||||
buffer = module.Loki_log_buffer(
|
||||
flexmock(),
|
||||
dry_run=False,
|
||||
tls_cert_path='/path/to/cert.crt',
|
||||
tls_key_path='/path/to/key.key',
|
||||
)
|
||||
|
||||
assert buffer.tls_cert_path == '/path/to/cert.crt'
|
||||
assert buffer.tls_key_path == '/path/to/key.key'
|
||||
|
||||
|
||||
def test_loki_log_handler_init_with_tls_passes_paths_to_buffer():
|
||||
handler = module.Loki_log_handler(
|
||||
flexmock(),
|
||||
send_logs=False,
|
||||
log_level=10,
|
||||
dry_run=False,
|
||||
tls_cert_path='/path/to/cert.crt',
|
||||
tls_key_path='/path/to/key.key',
|
||||
)
|
||||
|
||||
assert handler.buffer.tls_cert_path == '/path/to/cert.crt'
|
||||
assert handler.buffer.tls_key_path == '/path/to/key.key'
|
||||
|
||||
|
||||
def test_initialize_monitor_with_only_cert_path_raises():
|
||||
hook_config = {
|
||||
'url': 'http://localhost:3100/loki/api/v1/push',
|
||||
'tls': {'cert_path': '/path/to/cert.crt'},
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.initialize_monitor(hook_config, {}, 'test.yaml', 1, False)
|
||||
|
||||
|
||||
def test_initialize_monitor_with_only_key_path_raises():
|
||||
hook_config = {
|
||||
'url': 'http://localhost:3100/loki/api/v1/push',
|
||||
'tls': {'key_path': '/path/to/key.key'},
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.initialize_monitor(hook_config, {}, 'test.yaml', 1, False)
|
||||
|
witten marked this conversation as resolved
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue
Nice tests!!