Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for TLS configuration at MAAS charm #220

Merged
merged 27 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7601231
Add ssl_cert and ssl_key config items, with validation. Run maas conf…
wyattrees Aug 28, 2024
256325f
No prompt for maas config-tls
wyattrees Aug 30, 2024
4bde12f
update haproxy before running maas config-tls
wyattrees Aug 30, 2024
6e7ebd0
wait to configure tls so that relation has time to apply
wyattrees Sep 3, 2024
2260c4c
debug
wyattrees Sep 3, 2024
042a9cf
Change ssl_cert and ssl_key config options to be the content of the f…
wyattrees Sep 4, 2024
0969577
cleanup, add tests
wyattrees Sep 4, 2024
72ca0bb
Make default value of tls_mode 'disabled' instead of emtpy string
wyattrees Sep 5, 2024
2208f8b
use process substitution instead of creating tls files
wyattrees Sep 6, 2024
31ccd2f
Revert "use process substitution instead of creating tls files"
wyattrees Sep 9, 2024
69f2cae
Add cacert config option for self-signed certificates
wyattrees Sep 9, 2024
29d7808
Fix failing test
wyattrees Sep 25, 2024
b73f0ad
Add ignore type comments for passing config items to create_tls_files…
wyattrees Sep 25, 2024
ff30ebb
Delete TLS files after running maas config-tls enable. add logic to d…
wyattrees Oct 3, 2024
f34effd
Add check whether MAAS is initialized yet in _on_config_changed, upda…
wyattrees Oct 4, 2024
3d77f8c
only run update tls config in _initialize_maas the first time
wyattrees Oct 8, 2024
68aa3b3
rename maas_initialized variable to be more representative of what it…
wyattrees Oct 8, 2024
be53a7d
Use stored state to track initialization of MAAS
wyattrees Oct 30, 2024
5df465d
initialize _stored var
wyattrees Oct 30, 2024
ba722ac
use peer relation data so that only the leader enables or disables TLS
wyattrees Oct 31, 2024
ccfc214
Use maas_api_url to detect if MAAS has been initialized before enabli…
wyattrees Nov 7, 2024
3ebd5dc
handle nginx config not being present
wyattrees Nov 7, 2024
d96218a
return None if MAAS is not initialized yet in MaasHelper.is_tls_enabl…
wyattrees Nov 7, 2024
1a3395c
Use Path instead of raw str for filepaths in helper.py. Up pyright py…
wyattrees Nov 8, 2024
0f9be7c
set python version to 3.10 in tox.ini and pyproject.toml
wyattrees Nov 8, 2024
be799cf
Add unittests for new MaasHelper function, last change for python ver…
wyattrees Nov 8, 2024
6974875
revert python version changes
wyattrees Nov 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion maas-region/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ parts:
config:
options:
tls_mode:
default: "disabled"
description: Whether to enable TLS termination at HA Proxy ('termination'), at MAAS ('passthrough'), or no TLS ('disabled')
type: string
ssl_cert_content:
default: ""
description: Whether to enable TLS termination at HA Proxy ('termination'), or no TLS ('')
description: SSL certificate for tls_mode='passthrough'
type: string
ssl_key_content:
default: ""
description: SSL private key for tls_mode='passthrough'
type: string
31 changes: 26 additions & 5 deletions maas-region/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ class MaasRegionCharm(ops.CharmBase):
"""Charm the application."""

_TLS_MODES = [
"",
"disabled",
"termination",
] # no TLS, termination at HA Proxy
"passthrough",
] # no TLS, termination at HA Proxy, passthrough to MAAS

def __init__(self, *args):
super().__init__(*args)
Expand Down Expand Up @@ -234,6 +235,11 @@ def _initialize_maas(self) -> bool:
MaasHelper.setup_region(
self.maas_api_url, self.connection_string, self.get_operational_mode()
)
if self.config["tls_mode"] == "passthrough":
MaasHelper.create_tls_files(
self.config["ssl_cert_content"], self.config["ssl_key_content"]
)
MaasHelper.config_tls()
return True
except subprocess.CalledProcessError:
return False
Expand All @@ -257,6 +263,9 @@ def _get_regions(self) -> List[str]:
return list(set(eps))

def _update_ha_proxy(self) -> None:
region_port = (
MAAS_HTTPS_PORT if self.config["tls_mode"] == "passthrough" else MAAS_HTTP_PORT
)
if relation := self.model.get_relation(MAAS_API_RELATION):
app_name = f"api-{self.app.name}"
data = [
Expand All @@ -269,13 +278,13 @@ def _update_ha_proxy(self) -> None:
(
f"{app_name}-{self.unit.name.replace('/', '-')}",
self.bind_address,
MAAS_HTTP_PORT,
region_port,
[],
)
],
},
]
if self.config["tls_mode"] == "termination":
if self.config["tls_mode"] != "disabled":
data.append(
{
"service_name": "agent_service",
Expand All @@ -291,7 +300,6 @@ def _update_ha_proxy(self) -> None:
],
}
)
# TODO: Implement passthrough configuration
relation.data[self.unit]["services"] = yaml.safe_dump(data)

def _on_start(self, _event: ops.StartEvent) -> None:
Expand Down Expand Up @@ -434,11 +442,24 @@ def _on_get_api_endpoint_action(self, event: ops.ActionEvent):
event.fail("MAAS is not initialized yet")

def _on_config_changed(self, event: ops.ConfigChangedEvent):
# validate tls_mode
tls_mode = self.config["tls_mode"]
if tls_mode not in self._TLS_MODES:
msg = f"Invalid tls_mode configuration: '{tls_mode}'. Valid options are: {self._TLS_MODES}"
self.unit.status = ops.BlockedStatus(msg)
raise ValueError(msg)
# validate certificate and key
if tls_mode == "passthrough":
cert = self.config["ssl_cert_content"]
key = self.config["ssl_key_content"]
if not cert or not key:
raise ValueError(
"Both ssl_cert_content and ssl_key_content must be defined when using tls_mode=passthrough"
)
if "BEGIN CERTIFICATE" not in self.config["ssl_cert_content"]:
raise ValueError("Invalid SSL certificate")
if "BEGIN PRIVATE KEY" not in self.config["ssl_key_content"]:
raise ValueError("Invalid SSL private key file")
self._update_ha_proxy()


Expand Down
39 changes: 39 additions & 0 deletions maas-region/src/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

"""Helper functions for MAAS management."""

import logging
import subprocess
from os.path import exists
from pathlib import Path
from typing import Union

Expand All @@ -14,6 +16,10 @@
MAAS_SECRET = Path("/var/snap/maas/common/maas/secret")
MAAS_ID = Path("/var/snap/maas/common/maas/maas_id")
MAAS_SERVICE = "pebble"
MAAS_SSL_CERT_FILEPATH = "/var/snap/maas/common/cert.pem"
MAAS_SSL_KEY_FILEPATH = "/var/snap/maas/common/key.pem"

logger = logging.getLogger(__name__)


class MaasHelper:
Expand Down Expand Up @@ -186,6 +192,39 @@
]
subprocess.check_call(cmd)

@staticmethod
def create_tls_files(ssl_certificate: str, ssl_key: str, overwrite: bool = False) -> None:
"""Ensure that the SSL certificate and private key exist.

Args:
ssl_certificate (str): contents of the certificate file
ssl_key (str): contents of the private key file
overwrite (bool): Whether to overwrite the files if they exist already
"""
if not exists(MAAS_SSL_CERT_FILEPATH) or overwrite:
with open(MAAS_SSL_CERT_FILEPATH, "w") as cert_file:
cert_file.write(ssl_certificate)
if not exists(MAAS_SSL_KEY_FILEPATH) or overwrite:
with open(MAAS_SSL_KEY_FILEPATH, "w") as key_file:
key_file.write(ssl_key)

@staticmethod
def config_tls() -> None:
"""Set up TLS for the Region controller.

Raises:
CalledProcessError: if "maas config-tls enable" command failed for any reason
"""
cmd = [
"/snap/bin/maas",
"config-tls",
"enable",
"--yes",
MAAS_SSL_KEY_FILEPATH,
MAAS_SSL_CERT_FILEPATH,
]
subprocess.check_call(cmd)

@staticmethod
def get_maas_secret() -> Union[str, None]:
"""Get MAAS enrollment secret token.
Expand Down
38 changes: 37 additions & 1 deletion maas-region/tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def test_ha_proxy_data(self, mock_helper):
self.assertEqual(ha_data[0]["servers"][0][1], "10.0.0.10")

@patch("charm.MaasHelper", autospec=True)
def test_ha_proxy_data_tls(self, mock_helper):
def test_ha_proxy_data_tls_termination(self, mock_helper):
self.harness.set_leader(True)
self.harness.update_config({"tls_mode": "termination"})
self.harness.begin()
Expand All @@ -134,6 +134,30 @@ def test_ha_proxy_data_tls(self, mock_helper):
self.assertIn("service_host", ha_data[1]) # codespell:ignore
self.assertEqual(len(ha_data[1]["servers"]), 1)
self.assertEqual(ha_data[1]["servers"][0][1], "10.0.0.10")
self.assertEqual(ha_data[0]["servers"][0][2], 5240)

@patch("charm.MaasHelper", autospec=True)
def test_ha_proxy_data_tls_passthrough(self, mock_helper):
self.harness.set_leader(True)
self.harness.update_config(
{
"tls_mode": "passthrough",
"ssl_cert_content": "BEGIN CERTIFICATE",
"ssl_key_content": "BEGIN_PRIVATE_KEY",
}
)
self.harness.begin()
ha = self.harness.add_relation(
MAAS_API_RELATION, "haproxy", unit_data={"public-address": "proxy.maas"}
)

ha_data = yaml.safe_load(self.harness.get_relation_data(ha, "maas-region/0")["services"])
self.assertEqual(len(ha_data), 2)
self.assertIn("service_name", ha_data[1]) # codespell:ignore
self.assertIn("service_host", ha_data[1]) # codespell:ignore
self.assertEqual(len(ha_data[1]["servers"]), 1)
self.assertEqual(ha_data[1]["servers"][0][1], "10.0.0.10")
self.assertEqual(ha_data[0]["servers"][0][2], 5443)

@patch("charm.MaasHelper", autospec=True)
def test_invalid_tls_mode(self, mock_helper):
Expand All @@ -148,6 +172,18 @@ def test_invalid_tls_mode(self, mock_helper):
ha_data = yaml.safe_load(self.harness.get_relation_data(ha, "maas-region/0")["services"])
self.assertEqual(len(ha_data), 1)

@patch("charm.MaasHelper", autospec=True)
def test_bad_ssl_cert_key_config(self, mock_helper):
self.harness.set_leader(True)
self.harness.begin()
self.harness.add_relation(
MAAS_API_RELATION, "haproxy", unit_data={"public-address": "proxy.maas"}
)
with self.assertRaises(ValueError):
self.harness.update_config(
{"tls_mode": "passthrough", "ssl_cert_content": "blah", "ssl_key_content": "bleh"}
)

@patch("charm.MaasHelper", autospec=True)
def test_on_maas_cluster_changed_new_agent(self, mock_helper):
mock_helper.get_maas_mode.return_value = "region"
Expand Down
Loading