Merge ~troyanov/maas:backport-76e0fb0-3.5 into maas:3.5

Proposed by Anton Troyanov
Status: Merged
Approved by: Anton Troyanov
Approved revision: f3e448f14e694de6cc4de3e98419b079baa4745f
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~troyanov/maas:backport-76e0fb0-3.5
Merge into: maas:3.5
Diff against target: 324 lines (+156/-12)
8 files modified
debian/maas-region-api.maas-temporal.service (+2/-1)
src/maasagent/cmd/maas-agent/main.go (+36/-0)
src/maasagent/cmd/maas-agent/main_test.go (+30/-0)
src/maasserver/regiondservices/temporal.py (+28/-8)
src/maasserver/templates/temporal/production-dynamic.yaml.template (+4/-0)
src/maasserver/templates/temporal/production.yaml.template (+38/-1)
src/maasserver/tests/test_commands_config_tls.py (+1/-1)
src/maasserver/workflow/worker/worker.py (+17/-1)
Reviewer Review Type Date Requested Status
Anton Troyanov Approve
MAAS Lander unittests Pending
Review via email: mp+463664@code.launchpad.net

Commit message

feat(temporal): enable mTLS

Use MAAS cluster key/cert and CA to configure Temporal mTLS [0]

[0]: https://docs.temporal.io/self-hosted-guide/security#encryption-in-transit-with-mtls

Resolves LP:2058332

(cherry picked from commit 76e0fb0351f31aa9e4f3c87a78792c8cec20ef0d)

To post a comment you must log in.
Revision history for this message
Anton Troyanov (troyanov) wrote :

Self approving backport

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/maas-region-api.maas-temporal.service b/debian/maas-region-api.maas-temporal.service
2index f99026f..91aa21c 100644
3--- a/debian/maas-region-api.maas-temporal.service
4+++ b/debian/maas-region-api.maas-temporal.service
5@@ -9,7 +9,8 @@ ConditionPathIsDirectory=/var/lib/maas/temporal
6 User=maas
7 Group=maas
8 KillMode=mixed
9-# XXX: usage of --allow-no-auth is temporary for experimental Temporal support
10+
11+# --allow-no-auth flag to allow noopAuthorizer
12 ExecStart=/usr/sbin/temporal-server \
13 -e production \
14 -r "/var/lib/maas/temporal/" \
15diff --git a/src/maasagent/cmd/maas-agent/main.go b/src/maasagent/cmd/maas-agent/main.go
16index d2b6490..5ff28ea 100644
17--- a/src/maasagent/cmd/maas-agent/main.go
18+++ b/src/maasagent/cmd/maas-agent/main.go
19@@ -7,6 +7,8 @@ package main
20
21 import (
22 "context"
23+ "crypto/tls"
24+ "crypto/x509"
25 "fmt"
26 "os"
27 "os/signal"
28@@ -86,6 +88,22 @@ func Run() int {
29 clientBackoff := backoff.NewExponentialBackOff()
30 clientBackoff.MaxElapsedTime = 60 * time.Second
31
32+ certsDir := getCertificatesDir()
33+
34+ cert, err := tls.LoadX509KeyPair(fmt.Sprintf("%s/cluster.pem", certsDir), fmt.Sprintf("%s/cluster.key", certsDir))
35+ if err != nil {
36+ log.Error().Err(err).Msg("Failed loading client cert and key")
37+ }
38+
39+ ca := x509.NewCertPool()
40+
41+ b, err := os.ReadFile(fmt.Sprintf("%s/cacerts.pem", certsDir))
42+ if err != nil {
43+ log.Error().Err(err).Msg("Failed reading CA")
44+ } else if !ca.AppendCertsFromPEM(b) {
45+ log.Error().Err(err).Msg("CA PEM file is invalid")
46+ }
47+
48 temporalClient, err := backoff.RetryWithData(
49 func() (client.Client, error) {
50 return client.Dial(client.Options{
51@@ -97,6 +115,14 @@ func Run() int {
52 converter.GetDefaultDataConverter(),
53 codec,
54 ),
55+ ConnectionOptions: client.ConnectionOptions{
56+ TLS: &tls.Config{
57+ MinVersion: tls.VersionTLS12,
58+ Certificates: []tls.Certificate{cert},
59+ RootCAs: ca,
60+ ServerName: "maas",
61+ },
62+ },
63 })
64 }, clientBackoff,
65 )
66@@ -227,6 +253,16 @@ func getRunDir() string {
67 return "/run/maas"
68 }
69
70+func getCertificatesDir() string {
71+ dataDir := os.Getenv("SNAP_DATA")
72+
73+ if dataDir != "" {
74+ return fmt.Sprintf("%s/certificates", dataDir)
75+ }
76+
77+ return "/var/lib/maas/certificates"
78+}
79+
80 func main() {
81 os.Exit(Run())
82 }
83diff --git a/src/maasagent/cmd/maas-agent/main_test.go b/src/maasagent/cmd/maas-agent/main_test.go
84index 505b348..8c5f0df 100644
85--- a/src/maasagent/cmd/maas-agent/main_test.go
86+++ b/src/maasagent/cmd/maas-agent/main_test.go
87@@ -34,3 +34,33 @@ func TestGetRunDir(t *testing.T) {
88 })
89 }
90 }
91+
92+func TestCertificatesDir(t *testing.T) {
93+ testcases := map[string]struct {
94+ in func(t *testing.T)
95+ out string
96+ }{
97+ "snap": {
98+ in: func(t *testing.T) {
99+ t.Setenv("SNAP_DATA", "/var/snap/maas/x1")
100+ },
101+ out: "/var/snap/maas/x1/certificates",
102+ },
103+ "deb": {
104+ in: func(t *testing.T) {
105+ t.Setenv("SNAP_DATA", "")
106+ }, out: "/var/lib/maas/certificates",
107+ },
108+ }
109+
110+ for name, tc := range testcases {
111+ tc := tc
112+
113+ t.Run(name, func(t *testing.T) {
114+ tc.in(t)
115+
116+ res := getCertificatesDir()
117+ assert.Equal(t, tc.out, res)
118+ })
119+ }
120+}
121diff --git a/src/maasserver/regiondservices/temporal.py b/src/maasserver/regiondservices/temporal.py
122index 5220dac..c00eeb3 100644
123--- a/src/maasserver/regiondservices/temporal.py
124+++ b/src/maasserver/regiondservices/temporal.py
125@@ -17,6 +17,7 @@ from twisted.internet.defer import inlineCallbacks
126 from maasserver.config import RegionConfiguration
127 from maasserver.service_monitor import service_monitor
128 from maasserver.utils import load_template
129+from provisioningserver.certificates import get_maas_cluster_cert_paths
130 from provisioningserver.logger import get_maas_logger
131 from provisioningserver.path import get_maas_data_path
132 from provisioningserver.utils.env import MAAS_ID
133@@ -33,6 +34,9 @@ class RegionTemporalService(Service):
134 def _configure(self):
135 """Update the Temporal configuration for the Temporal service."""
136 template = load_template("temporal", "production.yaml.template")
137+ dynamic_template = load_template(
138+ "temporal", "production-dynamic.yaml.template"
139+ )
140
141 # Can't use the public attribute since it hits
142 # maasserver.utils.orm.DisabledDatabaseConnection
143@@ -64,6 +68,15 @@ class RegionTemporalService(Service):
144 f"Please consider setting it manually using regiond.conf"
145 )
146
147+ temporal_config_dir = Path(
148+ os.environ.get(
149+ "MAAS_TEMPORAL_CONFIG_DIR", get_maas_data_path("temporal")
150+ )
151+ )
152+ temporal_config_dir.mkdir(parents=True, exist_ok=True)
153+
154+ cert_file, key_file, cacert_file = get_maas_cluster_cert_paths()
155+
156 environ = {
157 "database": dbconf["NAME"],
158 "user": dbconf.get("USER", ""),
159@@ -71,20 +84,27 @@ class RegionTemporalService(Service):
160 "address": f"{host}:{dbconf['PORT']}",
161 "connect_attributes": connection_attributes,
162 "broadcast_address": broadcast_address,
163+ "config_dir": str(temporal_config_dir),
164+ "cert_file": cert_file,
165+ "key_file": key_file,
166+ "cacert_file": cacert_file,
167 }
168
169- rendered = template.substitute(environ).encode()
170+ rendered_template = template.substitute(environ).encode()
171+ rendered_dynamic_template = dynamic_template.substitute(
172+ environ
173+ ).encode()
174
175- temporal_config_dir = Path(
176- os.environ.get(
177- "MAAS_TEMPORAL_CONFIG_DIR", get_maas_data_path("temporal")
178- )
179+ atomic_write(
180+ rendered_template,
181+ temporal_config_dir / "production.yaml",
182+ overwrite=True,
183+ mode=0o600,
184 )
185- temporal_config_dir.mkdir(parents=True, exist_ok=True)
186
187 atomic_write(
188- rendered,
189- temporal_config_dir / "production.yaml",
190+ rendered_dynamic_template,
191+ temporal_config_dir / "production-dynamic.yaml",
192 overwrite=True,
193 mode=0o600,
194 )
195diff --git a/src/maasserver/templates/temporal/production-dynamic.yaml.template b/src/maasserver/templates/temporal/production-dynamic.yaml.template
196new file mode 100644
197index 0000000..177adfd
198--- /dev/null
199+++ b/src/maasserver/templates/temporal/production-dynamic.yaml.template
200@@ -0,0 +1,4 @@
201+# EnableRingpopTLS controls whether to use TLS for ringpop, using the same "internode" TLS
202+# config as the other services.
203+system.enableRingpopTLS:
204+ - value: true
205diff --git a/src/maasserver/templates/temporal/production.yaml.template b/src/maasserver/templates/temporal/production.yaml.template
206index 7cf4ba7..ae8e478 100644
207--- a/src/maasserver/templates/temporal/production.yaml.template
208+++ b/src/maasserver/templates/temporal/production.yaml.template
209@@ -1,6 +1,6 @@
210 log:
211 stdout: true
212- level: info
213+ level: warn
214
215 persistence:
216 defaultStore: default
217@@ -48,6 +48,40 @@ global:
218 pprof:
219 # disable pprof
220 port: 0
221+ tls:
222+ refreshInterval: 0s
223+ internode:
224+ # This server section configures the TLS certificate that internal temporal
225+ # cluster nodes (history, matching, and internal-frontend) present to other
226+ # clients within the Temporal Cluster.
227+ server:
228+ requireClientAuth: true
229+ certFile: {{ cert_file }}
230+ keyFile: {{ key_file }}
231+ clientCaFiles:
232+ - {{ cacert_file }}
233+ # This client section is used to configure the TLS clients within
234+ # the Temporal Cluster that connect to an Internode (history, matching, or
235+ # internal-frontend)
236+ client:
237+ serverName: maas
238+ disableHostVerification: false
239+ rootCaFiles:
240+ - {{ cacert_file }}
241+ frontend:
242+ # This server section configures the TLS certificate that the Frontend
243+ # server presents to external clients.
244+ server:
245+ requireClientAuth: true
246+ certFile: {{ cert_file }}
247+ keyFile: {{ key_file }}
248+ clientCaFiles:
249+ - {{ cacert_file }}
250+ client:
251+ serverName: maas
252+ disableHostVerification: false
253+ rootCaFiles:
254+ - {{ cacert_file }}
255 metrics:
256 prometheus:
257 framework: "tally"
258@@ -92,3 +126,6 @@ clusterMetadata:
259 rpcName: "frontend"
260 rpcAddress: "localhost:7233"
261
262+dynamicConfigClient:
263+ filepath: "{{ config_dir }}/production-dynamic.yaml"
264+ pollInterval: "60s"
265diff --git a/src/maasserver/tests/test_commands_config_tls.py b/src/maasserver/tests/test_commands_config_tls.py
266index f7bcd2f..be2c040 100644
267--- a/src/maasserver/tests/test_commands_config_tls.py
268+++ b/src/maasserver/tests/test_commands_config_tls.py
269@@ -100,7 +100,7 @@ class TestConfigTLSCommand(MAASServerTestCase):
270 def test_config_tls_enable_with_cacert(self):
271 sample_cert = get_sample_cert_with_cacerts()
272 cert_path, key_path = sample_cert.tempfiles()
273- cacert_path = Path(self.make_dir()) / "cacert.pem"
274+ cacert_path = Path(self.make_dir()) / "cacerts.pem"
275 cacert_path.write_text(sample_cert.ca_certificates_pem())
276
277 call_command(
278diff --git a/src/maasserver/workflow/worker/worker.py b/src/maasserver/workflow/worker/worker.py
279index ec0391e..99743c8 100644
280--- a/src/maasserver/workflow/worker/worker.py
281+++ b/src/maasserver/workflow/worker/worker.py
282@@ -12,13 +12,14 @@ from temporalio.api.workflowservice.v1 import (
283 DescribeNamespaceRequest,
284 RegisterNamespaceRequest,
285 )
286-from temporalio.client import Client
287+from temporalio.client import Client, TLSConfig
288 import temporalio.converter
289 from temporalio.service import RPCError, RPCStatusCode
290 from temporalio.worker import Worker as TemporalWorker
291
292 from maasserver.utils.asynchronous import async_retry
293 from maasserver.workflow.codec.encryptor import EncryptionCodec
294+from provisioningserver.certificates import get_maas_cluster_cert_paths
295 from provisioningserver.utils.env import MAAS_ID, MAAS_SHARED_SECRET
296
297 REGION_TASK_QUEUE = "region-internal"
298@@ -32,6 +33,15 @@ TEMPORAL_NAMESPACE = "default"
299 async def get_client_async() -> Client:
300 maas_id = MAAS_ID.get()
301 pid = os.getpid()
302+ cert_file, key_file, cacert_file = get_maas_cluster_cert_paths()
303+
304+ with open(cert_file, "rb") as f:
305+ cert = f.read()
306+ with open(key_file, "rb") as f:
307+ key = f.read()
308+ with open(cacert_file, "rb") as f:
309+ cacert = f.read()
310+
311 return await Client.connect(
312 f"{TEMPORAL_HOST}:{TEMPORAL_PORT}",
313 identity=f"{maas_id}@region:{pid}",
314@@ -39,6 +49,12 @@ async def get_client_async() -> Client:
315 temporalio.converter.default(),
316 payload_codec=EncryptionCodec(MAAS_SHARED_SECRET.get().encode()),
317 ),
318+ tls=TLSConfig(
319+ domain="maas",
320+ server_root_ca_cert=cacert,
321+ client_cert=cert,
322+ client_private_key=key,
323+ ),
324 )
325
326

Subscribers

People subscribed via source and target branches