Skip to content

Commit

Permalink
fix: Uses egress subnet list to configure approle (#450)
Browse files Browse the repository at this point in the history
  • Loading branch information
saltiyazan authored Aug 7, 2024
1 parent 9acbad2 commit 68d4bf6
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 75 deletions.
88 changes: 59 additions & 29 deletions lib/charms/vault_k8s/v0/vault_kv.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ def _on_install(self, event: InstallEvent):
def _on_connected(self, event: vault_kv.VaultKvConnectedEvent):
relation = self.model.get_relation(event.relation_name, event.relation_id)
egress_subnet = str(self.model.get_binding(relation).network.interfaces[0].subnet)
self.interface.request_credentials(relation, egress_subnet, self.get_nonce())
egress_subnets = [str(subnet) for subnet in self.model.get_binding(relation).network.egress_subnets][0].subnet]
egress_subnets.append(str(self.model.get_binding(relation).network.interfaces[0].subnet))
self.interface.request_credentials(relation, egress_subnets, self.get_nonce())
def _on_ready(self, event: vault_kv.VaultKvReadyEvent):
relation = self.model.get_relation(event.relation_name, event.relation_id)
Expand Down Expand Up @@ -94,9 +95,10 @@ def _on_update_status(self, event):
# Update status might not be the best place
binding = self.model.get_binding("vault-kv")
if binding is not None:
egress_subnet = str(binding.network.interfaces[0].subnet)
egress_subnets = [str(subnet) for subnet in self.model.get_binding(relation).network.egress_subnets][0].subnet]
egress_subnets.append(str(self.model.get_binding(relation).network.interfaces[0].subnet))
relation = self.model.get_relation(relation_name="vault-kv")
self.interface.request_credentials(relation, egress_subnet, self.get_nonce())
self.interface.request_credentials(relation, egress_subnets, self.get_nonce())
def get_nonce(self):
secret = self.model.get_secret(label=NONCE_SECRET_LABEL)
Expand Down Expand Up @@ -133,7 +135,7 @@ def get_nonce(self):

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 9
LIBPATCH = 10

PYDEPS = ["pydantic", "pytest-interface-tester"]

Expand Down Expand Up @@ -164,7 +166,7 @@ class VaultKvProviderSchema(BaseModel):
ca_certificate: str = Field(
description="The CA certificate to use when validating the Vault server's certificate."
)
egress_subnet: str = Field(description="The CIDR allowed by the role.")
egress_subnet: str = Field(description="The CIDRs allowed by the role separated by commas.")
credentials: Json[Mapping[str, str]] = Field(
description=(
"Mapping of unit name and credentials for that unit."
Expand All @@ -184,7 +186,9 @@ class AppVaultKvRequirerSchema(BaseModel):
class UnitVaultKvRequirerSchema(BaseModel):
"""Unit schema of the requirer side of the vault-kv interface."""

egress_subnet: str = Field(description="Egress subnet to use, in CIDR notation.")
egress_subnet: str = Field(
description="Egress subnets to use separated by commas, in CIDR notation."
)
nonce: str = Field(
description="Uniquely identifying value for this unit. `secrets.token_hex(16)` is recommended."
)
Expand All @@ -211,10 +215,21 @@ class KVRequest:
app_name: str
unit_name: str
mount_suffix: str
egress_subnet: str
egress_subnets: List[str]
nonce: str


def get_egress_subnets_list_from_relation_data(relation_databag: Mapping[str, str]) -> List[str]:
"""Return the egress_subnet as a list.
This function converts the string with values separated by commas to a list.
Args:
relation_databag: the relation databag of the unit or the app.
"""
return [subnet.strip() for subnet in relation_databag.get("egress_subnet", "").split(",")]


def is_requirer_data_valid(app_data: Mapping[str, str], unit_data: Mapping[str, str]) -> bool:
"""Return whether the requirer data is valid."""
try:
Expand All @@ -238,6 +253,12 @@ def is_provider_data_valid(data: Mapping[str, str]) -> bool:
return False


class VaultKvGoneAwayEvent(ops.EventBase):
"""VaultKvGoneAwayEvent Event."""

pass


class NewVaultKvClientAttachedEvent(ops.EventBase):
"""New vault kv client attached event."""

Expand All @@ -248,15 +269,15 @@ def __init__(
app_name: str,
unit_name: str,
mount_suffix: str,
egress_subnet: str,
egress_subnets: List[str],
nonce: str,
):
super().__init__(handle)
self.relation_id = relation_id
self.app_name = app_name
self.unit_name = unit_name
self.mount_suffix = mount_suffix
self.egress_subnet = egress_subnet
self.egress_subnets = egress_subnets
self.nonce = nonce

def snapshot(self) -> dict:
Expand All @@ -266,7 +287,7 @@ def snapshot(self) -> dict:
"app_name": self.app_name,
"unit_name": self.unit_name,
"mount_suffix": self.mount_suffix,
"egress_subnet": self.egress_subnet,
"egress_subnets": self.egress_subnets,
"nonce": self.nonce,
}

Expand All @@ -277,14 +298,15 @@ def restore(self, snapshot: Dict[str, Any]):
self.app_name = snapshot["app_name"]
self.unit_name = snapshot["unit_name"]
self.mount_suffix = snapshot["mount_suffix"]
self.egress_subnet = snapshot["egress_subnet"]
self.egress_subnets = snapshot["egress_subnets"]
self.nonce = snapshot["nonce"]


class VaultKvProviderEvents(ops.ObjectEvents):
"""List of events that the Vault Kv provider charm can leverage."""

new_vault_kv_client_attached = ops.EventSource(NewVaultKvClientAttachedEvent)
gone_away = ops.EventSource(VaultKvGoneAwayEvent)


class VaultKvProvides(ops.Object):
Expand All @@ -304,6 +326,10 @@ def __init__(
self.charm.on[relation_name].relation_changed,
self._on_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_broken,
self._on_vault_kv_relation_broken,
)

def _on_relation_changed(self, event: ops.RelationChangedEvent):
"""Handle client changed relation.
Expand All @@ -324,10 +350,16 @@ def _on_relation_changed(self, event: ops.RelationChangedEvent):
app_name=event.app.name,
unit_name=unit.name,
mount_suffix=event.relation.data[event.app]["mount_suffix"],
egress_subnet=event.relation.data[unit]["egress_subnet"],
egress_subnets=get_egress_subnets_list_from_relation_data(
event.relation.data[unit]
),
nonce=event.relation.data[unit]["nonce"],
)

def _on_vault_kv_relation_broken(self, event: ops.RelationBrokenEvent):
"""Handle relation broken."""
self.on.gone_away.emit()

def set_vault_url(self, relation: ops.Relation, vault_url: str):
"""Set the vault_url on the relation."""
if not self.charm.unit.is_leader():
Expand All @@ -354,11 +386,11 @@ def set_mount(self, relation: ops.Relation, mount: str):

relation.data[self.charm.app]["mount"] = mount

def set_egress_subnet(self, relation: ops.Relation, egress_subnet: str):
"""Set the egress_subnet on the relation."""
def set_egress_subnets(self, relation: ops.Relation, egress_subnets: List[str]):
"""Set the egress_subnets on the relation."""
if not self.charm.unit.is_leader():
return
relation.data[self.charm.app]["egress_subnet"] = egress_subnet
relation.data[self.charm.app]["egress_subnet"] = ",".join(egress_subnets)

def set_unit_credentials(
self,
Expand Down Expand Up @@ -439,7 +471,7 @@ def get_kv_requests(self, relation_id: Optional[int] = None) -> List[KVRequest]:
app_name=relation.app.name,
unit_name=unit.name,
mount_suffix=app_data["mount_suffix"],
egress_subnet=unit_data["egress_subnet"],
egress_subnets=get_egress_subnets_list_from_relation_data(unit_data),
nonce=unit_data["nonce"],
)
)
Expand Down Expand Up @@ -508,12 +540,6 @@ def restore(self, snapshot: Dict[str, Any]):
self.relation_name = snapshot["relation_name"]


class VaultKvGoneAwayEvent(ops.EventBase):
"""VaultKvGoneAwayEvent Event."""

pass


class VaultKvRequireEvents(ops.ObjectEvents):
"""List of events that the Vault Kv requirer charm can leverage."""

Expand Down Expand Up @@ -558,9 +584,9 @@ def _set_unit_nonce(self, relation: ops.Relation, nonce: str):
"""Set the nonce on the relation."""
relation.data[self.charm.unit]["nonce"] = nonce

def _set_unit_egress_subnet(self, relation: ops.Relation, egress_subnet: str):
"""Set the egress_subnet on the relation."""
relation.data[self.charm.unit]["egress_subnet"] = egress_subnet
def _set_unit_egress_subnets(self, relation: ops.Relation, egress_subnets: List[str]):
"""Set the egress_subnets on the relation."""
relation.data[self.charm.unit]["egress_subnet"] = ",".join(egress_subnets)

def _handle_relation(self, event: ops.EventBase):
"""Run when a new unit joins the relation or when the address of the unit changes.
Expand Down Expand Up @@ -597,16 +623,20 @@ def _on_vault_kv_relation_broken(self, event: ops.RelationBrokenEvent):
"""Handle relation broken."""
self.on.gone_away.emit()

def request_credentials(self, relation: ops.Relation, egress_subnet: str, nonce: str) -> None:
def request_credentials(
self, relation: ops.Relation, egress_subnet: Union[List[str], str], nonce: str
) -> None:
"""Request credentials from the vault-kv relation.
Generated secret ids are tied to the unit egress_subnet, so if the egress_subnet
changes a new secret id must be generated.
A change in egress_subnet can happen when the pod is rescheduled to a different
A change in egress_subnets can happen when the pod is rescheduled to a different
node by the underlying substrate without a change from Juju.
"""
self._set_unit_egress_subnet(relation, egress_subnet)
if isinstance(egress_subnet, str):
egress_subnet = [egress_subnet]
self._set_unit_egress_subnets(relation, egress_subnet)
self._set_unit_nonce(relation, nonce)

def get_vault_url(self, relation: ops.Relation) -> Optional[str]:
Expand Down
38 changes: 19 additions & 19 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ def _on_new_vault_kv_client_attached(self, event: NewVaultKvClientAttachedEvent)
app_name=event.app_name,
unit_name=event.unit_name,
mount_suffix=event.mount_suffix,
egress_subnet=event.egress_subnet,
egress_subnets=event.egress_subnets,
nonce=event.nonce,
)

Expand Down Expand Up @@ -607,7 +607,7 @@ def _sync_vault_kv(self) -> None:
app_name=kv_request.app_name,
unit_name=kv_request.unit_name,
mount_suffix=kv_request.mount_suffix,
egress_subnet=kv_request.egress_subnet,
egress_subnets=kv_request.egress_subnets,
nonce=kv_request.nonce,
)

Expand All @@ -617,7 +617,7 @@ def _generate_kv_for_requirer(
app_name: str,
unit_name: str,
mount_suffix: str,
egress_subnet: str,
egress_subnets: List[str],
nonce: str,
):
if not self.unit.is_leader():
Expand All @@ -633,8 +633,8 @@ def _generate_kv_for_requirer(
return
mount = f"charm-{app_name}-{mount_suffix}"
vault.enable_secrets_engine(SecretsBackend.KV_V2, mount)
self._ensure_unit_credentials(vault, relation, unit_name, mount, nonce, egress_subnet)
self._set_kv_relation_data(relation, mount, ca_certificate, egress_subnet)
self._ensure_unit_credentials(vault, relation, unit_name, mount, nonce, egress_subnets)
self._set_kv_relation_data(relation, mount, ca_certificate, egress_subnets)
self._remove_stale_nonce(relation=relation, nonce=nonce)

def _get_pki_ca_certificate(self) -> Optional[str]:
Expand Down Expand Up @@ -934,20 +934,20 @@ def _set_kv_relation_data(
relation: Relation,
mount: str,
ca_certificate: str,
egress_subnet: str,
egress_subnets: List[str],
) -> None:
"""Set relation data for vault-kv.
Args:
relation: Relation
mount: mount name
ca_certificate: CA certificate
egress_subnet: egress subnet
egress_subnets: egress subnet
"""
self.vault_kv.set_mount(relation, mount)
vault_url = self._get_relation_api_address(relation)
self.vault_kv.set_ca_certificate(relation, ca_certificate)
self.vault_kv.set_egress_subnet(relation, egress_subnet)
self.vault_kv.set_egress_subnets(relation, egress_subnets)
if vault_url is not None:
self.vault_kv.set_vault_url(relation, vault_url)

Expand All @@ -958,15 +958,15 @@ def _ensure_unit_credentials(
unit_name: str,
mount: str,
nonce: str,
egress_subnet: str,
egress_subnets: List[str],
):
"""Ensure a unit has credentials to access the vault-kv mount."""
policy_name = role_name = mount + "-" + unit_name.replace("/", "-")
vault.configure_policy(policy_name, "src/templates/kv_mount.hcl", mount=mount)
role_id = vault.configure_approle(
role_name,
policies=[policy_name],
cidrs=[egress_subnet],
cidrs=egress_subnets,
token_ttl="1h",
token_max_ttl="1h",
)
Expand All @@ -975,7 +975,7 @@ def _ensure_unit_credentials(
relation,
role_id,
role_name,
egress_subnet,
egress_subnets,
)
self.vault_kv.set_unit_credentials(relation, nonce, secret)

Expand All @@ -985,7 +985,7 @@ def _create_or_update_kv_secret(
relation: Relation,
role_id: str,
role_name: str,
egress_subnet: str,
egress_subnets: List[str],
) -> Secret:
"""Create or update a KV secret for a unit.
Expand All @@ -996,11 +996,11 @@ def _create_or_update_kv_secret(
secret_id = self._get_vault_kv_secret_in_peer_relation(label)
if secret_id is None:
return self._create_kv_secret(
vault, relation, role_id, role_name, egress_subnet, label
vault, relation, role_id, role_name, egress_subnets, label
)
else:
return self._update_kv_secret(
vault, relation, role_name, egress_subnet, label, secret_id
vault, relation, role_name, egress_subnets, label, secret_id
)

def _create_kv_secret(
Expand All @@ -1009,11 +1009,11 @@ def _create_kv_secret(
relation: Relation,
role_id: str,
role_name: str,
egress_subnet: str,
egress_subnets: List[str],
label: str,
) -> Secret:
"""Create a vault kv secret, store its id in the peer relation and return it."""
role_secret_id = vault.generate_role_secret_id(role_name, [egress_subnet])
role_secret_id = vault.generate_role_secret_id(role_name, egress_subnets)
secret = self.app.add_secret(
{"role-id": role_id, "role-secret-id": role_secret_id},
label=label,
Expand All @@ -1029,7 +1029,7 @@ def _update_kv_secret(
vault: Vault,
relation: Relation,
role_name: str,
egress_subnet: str,
egress_subnets: List[str],
label: str,
secret_id: str,
) -> Secret:
Expand All @@ -1039,9 +1039,9 @@ def _update_kv_secret(
credentials = secret.get_content(refresh=True)
role_secret_id_data = vault.read_role_secret(role_name, credentials["role-secret-id"])
# if unit subnet is already in cidr_list, skip
if egress_subnet in role_secret_id_data["cidr_list"]:
if sorted(egress_subnets) == sorted(role_secret_id_data["cidr_list"]):
return secret
credentials["role-secret-id"] = vault.generate_role_secret_id(role_name, [egress_subnet])
credentials["role-secret-id"] = vault.generate_role_secret_id(role_name, egress_subnets)
secret.set_content(credentials)
return secret

Expand Down
5 changes: 3 additions & 2 deletions tests/integration/vault_kv_requirer_operator/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ def _on_kv_connected(self, event: VaultKvConnectedEvent):
if not binding:
logger.error("Binding not found")
return
egress_subnet = str(binding.network.interfaces[0].subnet)
self.vault_kv.request_credentials(relation, egress_subnet, self.get_nonce())
egress_subnets = [str(subnet) for subnet in binding.network.egress_subnets]
egress_subnets.append(str(binding.network.interfaces[0].subnet))
self.vault_kv.request_credentials(relation, egress_subnets, self.get_nonce())

def _on_kv_ready(self, event: VaultKvReadyEvent):
"""Store the Vault KV credentials in a secret."""
Expand Down
Loading

0 comments on commit 68d4bf6

Please sign in to comment.