From 4eda45d36a58684d1c80c3355226f54a5a179139 Mon Sep 17 00:00:00 2001 From: Joshua Hesketh Date: Tue, 14 Dec 2021 17:40:27 +1100 Subject: [PATCH] ssl: Add initial support for supplying ssl certs Signed-off-by: Joshua Hesketh --- src/aquarium.py | 12 ++++++++++-- src/gravel/api/local.py | 31 +++++++++++++++++++++++++++++++ src/gravel/controllers/config.py | 21 ++++++++++++++++++++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/aquarium.py b/src/aquarium.py index 3effd88a5..e269434de 100755 --- a/src/aquarium.py +++ b/src/aquarium.py @@ -235,8 +235,16 @@ async def main_loop(self): async def start_uvicorn(self): logger.debug("Starting uvicorn") - # TODO(jhesketh): Check if we need https or not - uvicorn_config = Config(self.app, host="0.0.0.0", port=80) + if self.gstate.config.options.ssl.use_ssl: + uvicorn_config = Config( + self.app, + host="0.0.0.0", + port=443, + ssl_keyfile=self.gstate.config.ssl_keypath, + ssl_certfile=self.gstate.config.ssl_certpath, + ) + else: + uvicorn_config = Config(self.app, host="0.0.0.0", port=80) self.uvicorn = AquariumUvicorn(config=uvicorn_config) asyncio.create_task(self.uvicorn.serve()) # TODO(jhesketh): When in https mode, test if running a second uvicorn diff --git a/src/gravel/api/local.py b/src/gravel/api/local.py index e607a520d..5df4ff551 100644 --- a/src/gravel/api/local.py +++ b/src/gravel/api/local.py @@ -21,6 +21,7 @@ from gravel.api import jwt_auth_scheme from gravel.cephadm.models import VolumeDeviceModel +from gravel.controllers.config import SSLOptionsModel from gravel.controllers.gstate import GlobalState from gravel.controllers.inventory.nodeinfo import NodeInfoModel from gravel.controllers.nodes.deployment import NodeStageEnum @@ -51,6 +52,12 @@ class EventModel(BaseModel): message: str +class SSLConfigModel(BaseModel): + use_ssl: bool = Field(False, title="Whether https is enabled.") + key_contents: str = Field(title="The SSL private key.") + cert_contents: str = Field(title="The SSL certificate.") + + @router.get( "/volumes", name="Obtain local volumes", @@ -182,6 +189,30 @@ async def get_events( return [EventModel.parse_obj(event) for event in events] +@router.get( + "/ssl", + name="Get current SSL configuration", + response_model=SSLOptionsModel, +) +async def get_ssl_config( + request: Request, _: Callable = Depends(jwt_auth_scheme) +) -> SSLOptionsModel: + return request.app.state.gstate.config.options.ssl + + +@router.post("/ssl", name="Update SSL configuration") +async def apply_ssl_config( + request: Request, + req_params: SSLConfigModel, + _: Callable = Depends(jwt_auth_scheme), +) -> bool: + request.app.state.gstate.config.write_ssl_key(req_params.key_contents) + request.app.state.gstate.config.write_ssl_cert(req_params.cert_contents) + request.app.state.gstate.config.options.ssl.use_ssl = req_params.use_ssl + request.app.state.gstate.request_restart_uvicorn() + return request.app.state.gstate.config.saveConfig() + + @router.post("/reboot", name="Reboot the system") async def reboot(_: Callable = Depends(jwt_auth_scheme)) -> None: ret, _, stderr = await aqr_run_cmd(["systemctl", "reboot"]) # type: ignore diff --git a/src/gravel/controllers/config.py b/src/gravel/controllers/config.py index 014dac3b1..d11216d31 100644 --- a/src/gravel/controllers/config.py +++ b/src/gravel/controllers/config.py @@ -77,6 +77,10 @@ def get_image(self) -> str: return f"{self.registry}/{self.image}" +class SSLOptionsModel(BaseModel): + use_ssl: bool = Field(False, title="Whether https is enabled.") + + class OptionsModel(BaseModel): inventory: InventoryOptionsModel = Field(InventoryOptionsModel()) storage: StorageOptionsModel = Field(StorageOptionsModel()) @@ -85,6 +89,7 @@ class OptionsModel(BaseModel): network: NetworkOptionsModel = Field(NetworkOptionsModel()) auth: AuthOptionsModel = Field(AuthOptionsModel()) containers: ContainersOptionsModel = Field(ContainersOptionsModel()) + ssl: SSLOptionsModel = Field(SSLOptionsModel()) class ConfigModel(BaseModel): @@ -99,6 +104,8 @@ def __init__(self, path: Optional[str] = None): path = _get_default_confdir() self._confdir = Path(path) self.confpath = self._confdir.joinpath(Path("config.json")) + self.ssl_keypath = self._confdir.joinpath(Path("ssl_key.pem")) + self.ssl_certpath = self._confdir.joinpath(Path("ssl_cert.pem")) logger.debug(f"Aquarium config dir: {self._confdir}") self._confdir.mkdir(0o700, parents=True, exist_ok=True) @@ -133,9 +140,21 @@ def _registry_config_from_env(self): if len(keys) > 0: logger.warning("Using custom registry config from environment") - def _saveConfig(self, conf: ConfigModel) -> None: + def _saveConfig(self, conf: ConfigModel) -> bool: logger.debug(f"Writing Aquarium config: {self.confpath}") self.confpath.write_text(conf.json(indent=2)) + return True + + def saveConfig(self) -> bool: + return self._saveConfig(self.config) + + def write_ssl_key(self, contents: str) -> None: + self.ssl_keypath.write_text(contents) + self.ssl_keypath.chmod(0o600) + + def write_ssl_cert(self, contents: str) -> None: + self.ssl_certpath.write_text(contents) + self.ssl_certpath.chmod(0o644) @property def options(self) -> OptionsModel: