Skip to content

Commit 16da6b4

Browse files
committed
chore: run the key server in dev backend
We can use mitmproxy to terminate tls! the best way is to run a second mitmproxy, like we run a second nginx, and feed it the certs generated by the key-server. We need a little stuff in the key-server to support this because mitmproxy wants the whole bundle of tls cert, tls key, and ca cert in a single PEM (one PEM can contain multiple entities) and then we can "notify" of configuration changes by touching the main mitmproxy script since it has hot module reload. Probably. Nobody's going to run their dev backend instance for 3 years anyway. Closes EXEC-2524
1 parent fd113e0 commit 16da6b4

File tree

9 files changed

+106
-14
lines changed

9 files changed

+106
-14
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,4 @@ api/docs/dist/v2/
160160
package-testing/results
161161
all_analysis_results/
162162
auth-server/.env
163+
key-server/.key-server-storage

Makefile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,10 @@ dev-backend-flex:
349349
$(MAKE) -C auth-server dev ';' \
350350
$(MAKE) -C robot-server dev-flex OT_ROBOT_SERVER_auth_server_url=http://localhost:31950 BEHIND_DEV_PROXY=1 ';' \
351351
$(MAKE) -C system-server dev OT_SYSTEM_SERVER_auth_server_url=http://localhost:31950 ';' \
352-
$(MAKE) dev-proxy
352+
$(MAKE) -C key-server dev-mitmproxy ';' \
353+
$(MAKE) dev-proxy ';' \
354+
$(MAKE) dev-proxy-tls
355+
353356

354357
# Assuming our dev servers are running separately (make -C robot-server dev, make -C auth-server dev, etc.),
355358
# this sets up a reverse proxy that listens on localhost:31950 and forwards each request
@@ -367,3 +370,12 @@ dev-proxy:
367370
--mode reverse:http://localhost:2@31950 \
368371
--set connection_strategy=lazy \
369372
--script scripts/dev_proxy.py
373+
374+
.PHONY: dev-proxy-tls
375+
dev-proxy-tls:
376+
sleep 1
377+
$(UV) tool run --python $(UV_PYTHON) --from mitmproxy==12.2.1 mitmdump \
378+
--mode reverse:http://localhost:2@32313 \
379+
--set connection_strategy=lazy \
380+
--script scripts/dev_proxy.py \
381+
--certs key-server/.key-server-storage/tls/flex-certs.pem

key-server/Makefile

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ clean_cmd = $(SHX) rm -rf build .coverage coverage.xml '*.egg-info'
4242
clean_cache_cmd = $(SHX) rm -rf '**/__pycache__' '**/*.pyc' '**/.mypy_cache'
4343
clean_wheel_cmd = $(clean_cmd) dist/*.whl
4444
clean_all_cmd = $(clean_cmd) dist
45-
dev_port ?= "33950"
45+
dev_port ?= "33960"
4646

4747
.PHONY: all
4848
all: clean wheel
@@ -97,9 +97,16 @@ format:
9797
$(ruff) format .
9898
$(ruff) check --select I --fix .
9999

100+
# the dev task is pretty good for running the server just locally for quick tests.
100101
.PHONY: dev
101102
dev:
102-
OT_KEY_SERVER_dot_env_path=$(dir $(abspath $(lastword $(MAKEFILE_LIST))))/dev.env $(python) -m key_server --port $(dev_port) --reload --log-level=debug
103+
OT_KEY_SERVER_dot_env_path=dev.env $(python) -m key_server --port $(dev_port) --reload --log-level=debug
104+
105+
# the dev-mitmproxy task integrates with the whole-backend mode that runs a reverse proxy in
106+
# front of all the servers. don't run it unless you're going to run mitmproxy
107+
.PHONY: dev-mitmproxy
108+
dev-mitmproxy:
109+
OT_KEY_SERVER_dot_env_path=dev-mitmproxy.env $(python) -m key_server --port $(dev_port) --reload --log-level=debug
103110

104111
# Note: No `push` target, since this project only runs on Flex (OT-3), not OT-2.
105112

key-server/dev-mitmproxy.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
OT_KEY_SERVER_tls_server_integration=dev-mitmproxy
2+
OT_KEY_SERVER_base_directory=.key-server-storage
3+
OT_KEY_SERVER_tls_directory=.key-server-storage/tls
4+
OT_KEY_SERVER_mitmproxy_touch_path=../scripts/dev_proxy.py

key-server/key_server/config/dependency.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ def resolve_config() -> ResolvedConfig:
5757
secure_volume_size_mb=base_config.secure_volume_size_mb,
5858
tls_directory=tls_base_dir,
5959
tls_server_integration=base_config.tls_server_integration,
60+
mitmproxy_touch_path=(
61+
base_config.mitmproxy_touch_path
62+
if base_config.tls_server_integration == "dev-mitmproxy"
63+
else None
64+
),
6065
)
6166

6267

key-server/key_server/config/models.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,15 @@ class KeyServerConfig(BaseSettings):
5757
default="automatically_make_temporary",
5858
description="The location in which to store tls keys and certs for tls termination",
5959
)
60-
tls_server_integration: Literal["systemd-nginx", "dev-none"] = Field(
61-
description="How the server should notify a TLS termination layer a certificate has rotated.",
62-
default="systemd-nginx",
60+
tls_server_integration: Literal["systemd-nginx", "dev-none", "dev-mitmproxy"] = (
61+
Field(
62+
description="How the server should notify a TLS termination layer a certificate has rotated.",
63+
default="systemd-nginx",
64+
)
65+
)
66+
mitmproxy_touch_path: Path | None = Field(
67+
default=None,
68+
description="File to touch to notify mitmproxy it needs to rotate. Ignored if tls_server_integration is not dev-mitmproxy.",
6369
)
6470

6571

@@ -71,4 +77,5 @@ class ResolvedConfig(BaseModel):
7177
image_mount_point: Path
7278
secure_volume_size_mb: int
7379
tls_directory: Path
74-
tls_server_integration: Literal["systemd-nginx", "dev-none"]
80+
tls_server_integration: Literal["systemd-nginx", "dev-none", "dev-mitmproxy"]
81+
mitmproxy_touch_path: Path | None

key-server/key_server/tls/cryptography_utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,8 @@ def install_tls_cert(tls_cert_dir: Path, tls_cert: SignedCert) -> X509Pair:
220220
)
221221
tls_cert.keypath.unlink()
222222
return pair
223+
224+
225+
def make_pem_bundle(ca: X509Pair, tls: X509Pair, path: Path) -> None:
226+
"""Write a CA and TLS pair to a single file."""
227+
file_utils.make_pem_bundle(ca.cert, tls.cert, tls.key, path)

key-server/key_server/tls/file_utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,25 @@ def load_ca_cert(maybe_cert: Path) -> x509.Certificate | None: # noqa: C901
264264
)
265265
return None
266266
return cert
267+
268+
269+
def make_pem_bundle(
270+
ca_cert: x509.Certificate,
271+
tls_cert: x509.Certificate,
272+
tls_key: ec.EllipticCurvePrivateKey,
273+
path: Path,
274+
) -> None:
275+
"""Write a CA cert, end entity cert, and end entity key to a single PEM for certain TLS terminators."""
276+
path.parent.mkdir(parents=True, exist_ok=True)
277+
with path.open("wb") as combined_pem:
278+
combined_pem.write(
279+
tls_key.private_bytes(
280+
serialization.Encoding.PEM,
281+
serialization.PrivateFormat.PKCS8,
282+
serialization.NoEncryption(),
283+
)
284+
)
285+
combined_pem.write(b"\n")
286+
combined_pem.write(tls_cert.public_bytes(serialization.Encoding.PEM))
287+
combined_pem.write(b"\n")
288+
combined_pem.write(ca_cert.public_bytes(serialization.Encoding.PEM))

key-server/key_server/tls/manager.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import logging
55
from datetime import datetime, timedelta, timezone
66
from pathlib import Path
7-
from typing import Final, Literal, Self, Type
7+
from typing import Awaitable, Callable, Final, Literal, Self, Type, cast
88

99
from .ca_manager import TLSCAManager
10+
from .cryptography_utils import make_pem_bundle
1011
from .ee_manager import TLSEEManager
1112
from .robot_details.interface import RobotDetails
1213

@@ -37,7 +38,8 @@ async def create(
3738
ca_cert_dir: Path,
3839
ca_key_dir: Path,
3940
tls_ee_dir: Path,
40-
terminator_reload: Literal["systemd-nginx", "dev-none"],
41+
terminator_reload: Literal["systemd-nginx", "dev-none", "dev-mitmproxy"],
42+
mitmproxy_touch_path: Path | None = None,
4143
) -> Self:
4244
"""Create a TLS manager, taking several useful setup steps."""
4345
if terminator_reload == "systemd-nginx":
@@ -52,6 +54,12 @@ async def create(
5254

5355
robot_hostname, robot_ips = await robot_details.get_details()
5456
ca_manager = TLSCAManager(key_dir=ca_key_dir, ca_cert_dir=ca_cert_dir)
57+
mitm_rotator = _MITMProxyCertRotator(mitmproxy_touch_path)
58+
rotators = {
59+
"systemd-nginx": TLSEEManager.rotate_cert_nginx,
60+
"dev-none": TLSEEManager.rotate_cert_none,
61+
"dev-mitmproxy": cast(Callable[[], Awaitable[None]], mitm_rotator),
62+
}
5563
ee_manager = TLSEEManager(
5664
key_dir=tls_ee_dir,
5765
ee_cert_dir=tls_ee_dir,
@@ -61,16 +69,14 @@ async def create(
6169
# the easiest way is to start with no IP addresses and then to let the system that configures
6270
# IP addresses tell us when they start to exist.
6371
robot_ips=robot_ips,
64-
rotate_fn=(
65-
TLSEEManager.rotate_cert_nginx
66-
if terminator_reload == "systemd-nginx"
67-
else TLSEEManager.rotate_cert_none
68-
),
72+
rotate_fn=rotators[terminator_reload],
6973
)
7074

7175
obj = cls(
7276
ca_manager=ca_manager, ee_manager=ee_manager, robot_details=robot_details
7377
)
78+
if terminator_reload == "dev-mitmproxy":
79+
mitm_rotator.set_manager(obj)
7480
await obj.refresh_ee()
7581
await obj.schedule_expiry_task(cls.EXPIRY_CHECKER_POLL_PERIOD)
7682
await obj.schedule_robot_details_task()
@@ -179,3 +185,26 @@ async def _robot_details_task_outer(self) -> None:
179185
except BaseException:
180186
LOG.exception("Robot details task failed")
181187
raise
188+
189+
190+
class _MITMProxyCertRotator:
191+
def __init__(self, touch_path: Path | None) -> None:
192+
self._manager: TLSManager | None = None
193+
self._path = touch_path
194+
195+
def set_manager(self, manager: TLSManager) -> None:
196+
self._manager = manager
197+
198+
async def __call__(self) -> None:
199+
"""Rotate and tell the MITMProxy about it."""
200+
if not self._manager:
201+
return
202+
if not self._manager._ee_manager._ee_pair:
203+
return
204+
make_pem_bundle(
205+
self._manager._ca_manager._current_ca,
206+
self._manager._ee_manager._ee_pair,
207+
self._manager._ee_manager._ee_cert_dir / "flex-certs.pem",
208+
)
209+
if self._path:
210+
self._path.touch()

0 commit comments

Comments
 (0)