Skip to content

Commit

Permalink
Monitoring Entities (#36)
Browse files Browse the repository at this point in the history
* Make ExpirationMonitor an abstract class and subclass SecretExpirationMonitor from it

* Make generic entity_expiration_monitor

* Update documentation with entity monitoring info

* Add some entity expiration tests

* Bump version: 0.4.1 → 0.5.0

* Update README
  • Loading branch information
eugene-davis authored Jun 17, 2022
1 parent 1a136cf commit de54360
Show file tree
Hide file tree
Showing 18 changed files with 286 additions and 70 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.4.1
current_version = 0.5.0
commit = True
tag = False
message = Bump version: {current_version} → {new_version}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
workflow_dispatch:

env:
VERSION: 0.4.1
VERSION: 0.5.0

jobs:
release:
Expand Down
19 changes: 1 addition & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,7 @@ Additional authentication methods should be relatively easy to add due to usage

#### Required Policy

The exporter requires the `read` capability access to the metadata of the monitored secrets.
Additionally, if you are using the recursive function to monitor multiple secrets in a path, you will need to provide the `list` capability.

A sample policy for a secret in the KV2 engine `secret` at path `some/example/secret` would need a policy like:

```hcl
path "secret/metadata/some/example/secret" {
capabilities = [ "read" ]
}
```

To recursively monitor at the `example` level, it would look like:

```hcl
path "secret/metadata/some/example/**" {
capabilities = [ "read", "list" ]
}
```
Please see the [module documentation](#modules)

### Docker Image

Expand Down
4 changes: 2 additions & 2 deletions docs/CONFIGURATION_EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ vault:
authentication:
token:

secret_expiration_monitoring:
expiration_monitoring:
- name: simple_service
secrets:
- mount_point: secrets
Expand All @@ -48,7 +48,7 @@ vault:
refresh_interval: 10 # default is 30 seconds
port: 8350 # default is 9935

secret_expiration_monitoring:
expiration_monitoring:
metadata_fieldnames:
last_renewal_timestamp: "first_last_renewal_timestamp" # default is last_renewal_timestamp
expiration_timestamp: "first_expiration_timestamp" # default is expiration_timestamp
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "vault-assessment-prometheus-exporter"
version = "0.4.1"
version = "0.5.0"
description = "Prometheus exporter to monitor custom metadata for KV2 secrets for (self-imposed) expiration."
authors = ["Eugene Davis <[email protected]>"]
readme = "README.md"
Expand Down
87 changes: 87 additions & 0 deletions tests/test_entity_expiration_monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from datetime import datetime

import pytest
from mock import call
from pytest_mock import mocker

from vault_monitor.expiration_monitor import entity_expiration_monitor, expiration_monitor


@pytest.fixture(autouse=True)
def tear_down():
"""
Cleans out the mock gauges with every run
"""
yield
delattr(entity_expiration_monitor.EntityExpirationMonitor, "secret_last_renewal_timestamp_gauge")
delattr(entity_expiration_monitor.EntityExpirationMonitor, "secret_expiration_timestamp_gauge")


def test_basic_creation(mocker):
"""
Ensure all the basic setup works
"""
mock_vault_client = mocker.Mock()
mock_gauge = mocker.patch.object(expiration_monitor, "Gauge", autospec=True)

expected_labels = {"monitored_path": "monitored_path", "mount_point": "mount_point", "service": "service", "entity_name": "entity_name"}
test_object = entity_expiration_monitor.EntityExpirationMonitor(mount_point="mount_point", monitored_path="monitored_path", name="entity_name", vault_client=mock_vault_client, service="service")

assert test_object.prometheus_labels == expected_labels

assert test_object.last_renewed_timestamp_fieldname == "last_renewal_timestamp"
assert test_object.expiration_timestamp_fieldname == "expiration_timestamp"

gauge_calls = [
call("vault_entity_last_renewal_timestamp", "Timestamp for when an entity's secrets were last updated.", list(expected_labels.keys())),
call("vault_entity_expiration_timestamp", "Timestamp for when an entity's secrets should be expired and rotated.", list(expected_labels.keys())),
]

mock_gauge.assert_has_calls(gauge_calls)


def test_custom_values_creation(mocker):
"""
Set custom values for prometheus labels and metadata and ensure they are reflected in the class
"""
mock_vault_client = mocker.Mock()
mock_gauge = mocker.patch.object(expiration_monitor, "Gauge", autospec=True)

expected_labels = {"monitored_path": "monitored_path", "mount_point": "mount_point", "service": "service", "key": "value", "entity_name": "entity_name"}
test_object = entity_expiration_monitor.EntityExpirationMonitor(
mount_point="mount_point",
monitored_path="monitored_path",
name="entity_name",
vault_client=mock_vault_client,
service="service",
prometheus_labels={"key": "value"},
metadata_fieldnames={"last_renewal_timestamp": "some_other_renewal_name", "expiration_timestamp": "some_other_expiration_name"},
)

assert test_object.prometheus_labels == expected_labels

assert test_object.last_renewed_timestamp_fieldname == "some_other_renewal_name"
assert test_object.expiration_timestamp_fieldname == "some_other_expiration_name"

gauge_calls = [
call("vault_entity_last_renewal_timestamp", "Timestamp for when an entity's secrets were last updated.", list(expected_labels.keys())),
call("vault_entity_expiration_timestamp", "Timestamp for when an entity's secrets should be expired and rotated.", list(expected_labels.keys())),
]

mock_gauge.assert_has_calls(gauge_calls)


def test_get_expiraiton_info(mocker):
mock_vault_client = mocker.Mock()
mock_gauge = mocker.patch.object(expiration_monitor, "Gauge", autospec=True)

test_object = entity_expiration_monitor.EntityExpirationMonitor(mount_point="mount_point", monitored_path="monitored_path", name="entity_name", vault_client=mock_vault_client, service="service")

mock_response = mocker.Mock()
mock_response.json.return_value = {"data": {"metadata": {"last_renewal_timestamp": "2022-08-08T09:49:41.415869Z", "expiration_timestamp": "2022-08-08T09:49:41.415869Z"}}}
mock_requests = mocker.patch.object(entity_expiration_monitor, "requests", autospec=True)
mock_requests.get.return_value = mock_response

test_expiration_metadata = test_object.get_expiration_info()

assert test_expiration_metadata.get_serialized_expiration_metadata() == {"last_renewal_timestamp": "2022-08-08T09:49:41.415869Z", "expiration_timestamp": "2022-08-08T09:49:41.415869Z"}
16 changes: 8 additions & 8 deletions tests/test_expiration_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
from mock import call
from pytest_mock import mocker

from vault_monitor.secret_expiration_monitor import expiration_monitor
from vault_monitor.expiration_monitor import secret_expiration_monitor, expiration_monitor


@pytest.fixture(autouse=True)
def tear_down():
# Let test run first
yield
#
delattr(expiration_monitor.ExpirationMonitor, "secret_expiration_timestamp_gauge")
delattr(expiration_monitor.ExpirationMonitor, "secret_last_renewal_timestamp_gauge")
delattr(secret_expiration_monitor.SecretExpirationMonitor, "secret_expiration_timestamp_gauge")
delattr(secret_expiration_monitor.SecretExpirationMonitor, "secret_last_renewal_timestamp_gauge")


def test_basic_creation(mocker):
Expand All @@ -21,8 +21,8 @@ def test_basic_creation(mocker):
mock_vault_client = mocker.Mock()
mock_gauge = mocker.patch.object(expiration_monitor, "Gauge", autospec=True)

expected_labels = {"secret_path": "secret_path", "mount_point": "mount_point", "service": "service"}
test_object = expiration_monitor.ExpirationMonitor(mount_point="mount_point", secret_path="secret_path", vault_client=mock_vault_client, service="service")
expected_labels = {"monitored_path": "monitored_path", "mount_point": "mount_point", "service": "service"}
test_object = secret_expiration_monitor.SecretExpirationMonitor(mount_point="mount_point", monitored_path="monitored_path", vault_client=mock_vault_client, service="service")

assert test_object.prometheus_labels == expected_labels

Expand All @@ -44,10 +44,10 @@ def test_custom_values_creation(mocker):
mock_vault_client = mocker.Mock()
mock_gauge = mocker.patch.object(expiration_monitor, "Gauge", autospec=True)

expected_labels = {"secret_path": "secret_path", "mount_point": "mount_point", "service": "service", "key": "value"}
test_object = expiration_monitor.ExpirationMonitor(
expected_labels = {"monitored_path": "monitored_path", "mount_point": "mount_point", "service": "service", "key": "value"}
test_object = secret_expiration_monitor.SecretExpirationMonitor(
mount_point="mount_point",
secret_path="secret_path",
monitored_path="monitored_path",
vault_client=mock_vault_client,
service="service",
prometheus_labels={"key": "value"},
Expand Down
2 changes: 1 addition & 1 deletion tests/test_vault_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest
from pytest_mock import mocker

from vault_monitor.secret_expiration_monitor.vault_time import ExpirationMetadata
from vault_monitor.expiration_monitor.vault_time import ExpirationMetadata


@pytest.mark.parametrize("weeks, days, hours, minutes, seconds", [(1, 1, 1, 2, 4), (8, 10, 1105, 20, 4444)])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
# Secret Expiration Monitor Module
# Expiration Monitor Module

This module monitors custom metadata for Vault secrets to allow easy maintenance of secret expiration for non-dynamic secret types (i.e. KeyVault v2 secrets).
Additionally metadata can be added to Vault entity to track their secrets rotation (e.g. for AppRoles and their secret-ids).

## Vault Policy Requirements

The exporter requires the `read` capability access to the metadata of the monitored secrets.
Additionally `read` is required on the entities that are being monitored.
Additionally, if you are using the recursive function to monitor multiple secrets in a path, you will need to provide the `list` capability.

A sample policy for a secret in the KV2 engine `secret` at path `some/example/secret` would need a policy like:

```hcl
path "secret/metadata/some/example/secret" {
capabilities = [ "read" ]
}
```

To recursively monitor at the `example` level, it would look like:

```hcl
path "secret/metadata/some/example/**" {
capabilities = [ "read", "list" ]
}
```

## Set Expiration

Expiration and last-renewal information is stored in the custom metadata for each secret monitored.
Expiration and last-renewal information is stored in the custom metadata for each secrets, or in the metadata for each entity.
It is recommended that you use automation (for a script that is also importable as a Python module see below), however you can also set the metadata manually via CLI or UI or in any other automation system.

Both timestamps are in UTC time in the [ISO 8601 format](https://www.w3.org/TR/NOTE-datetime-970915) with the timezone (`Z`) included at the end - this matches the format the the Vault server itself uses for timestamps.
Expand All @@ -20,6 +43,9 @@ Keep in mind, both fieldnames can be customized if needed or desired, see [Metad

### Using the Provided Script

**Note**: The current script does not support setting entity ids.
This support will be added once they have been extended to support easier management of specific auth types, such as AppRole.

The `set_expiration` script can set the expiration data based on input provided (and can be imported/used as an example for automation).
It is capable of setting custom metadata fieldnames.
If you installed via poetry, you can execute `poetry run set_expiration -h` (or from a poetry shell just `set_expiration`) to see usage details.
Expand All @@ -45,7 +71,7 @@ Currently the Vault token is retrieved from the environment, in the near future

## Configuration

Configuration is set within the main configuration yaml file, under the key `secret_expiration_monitoring`
Configuration is set within the main configuration yaml file, under the key `expiration_monitoring`

### Metadata Fieldnames

Expand Down Expand Up @@ -76,3 +102,9 @@ Each service can contain the following:
* `mount_point` - secret engine mount point
* `secret_path` - path within the secret engine to the secret to monitor
* `recursive` (optional) - if this option is set, then any and all secrets within the `secret_path` will be monitored. Note that enabling this requires the list permission to be provided by Vault.

#### Entity Configuration

* `mount_point` - auth engine mount point
* `entity_id` - the entity id to monitor
* `entity_name` - a human readable name for the entity. This does not have to match the name used in Vault, as it is not used to look up the entity.
Loading

0 comments on commit de54360

Please sign in to comment.