Skip to content

Commit 566f60e

Browse files
asher-pem-armLuke Beardsmore
andcommitted
Add support for gRPC TLS server authentication
Signed-off-by: Asher Pemberton <asher.pemberton@arm.com> Reviewed-by: Asher Pemberton <asher.pemberton@arm.com> # gatekeeper Co-authored-by: Luke Beardsmore <luke.beardsmore2@arm.com>
1 parent 0507604 commit 566f60e

15 files changed

Lines changed: 613 additions & 20 deletions

doc/getting_started.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,26 @@ Follow these instructions to install the systemd files on your machine(s):
435435
436436
# usermod -a -G labgrid <user>
437437
438+
Enabling gRPC connection security
439+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
440+
441+
It is encouraged to use TLS for gRPC channels in a production environment.
442+
443+
This can be enabled on the ``labgrid-coordinator`` by adding the ``--tls``,
444+
``--cert`` and ``--key`` options.
445+
Refer to the ``labgrid-coordinator`` man page for details.
446+
447+
When you are connecting with ``labgrid-client`` or ``labgrid-exporter`` to a
448+
``labgrid-coordinator`` that has TLS gRPC channels enabled you need to pass
449+
the ``--tls`` option. If ``--cert`` is not set, labgrid uses the host CA
450+
certificates to verify the coordinator certificate. Use ``--cert`` to provide
451+
a specific CA certificate instead.
452+
Refer to the ``labgrid-client`` and ``labgrid-exporter`` man pages for details.
453+
For ``RemotePlace`` connections from an environment config, set the
454+
``coordinator_tls`` option or ``LG_COORDINATOR_TLS``. If ``coordinator_cert``
455+
is not set, labgrid uses the host CA certificates. Set ``coordinator_cert``
456+
to provide a specific CA certificate instead.
457+
438458
Using a Strategy
439459
----------------
440460

doc/man/client.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ LG_COORDINATOR
5454
This variable can be used to set the default coordinator in the format
5555
``HOST[:PORT]`` (instead of using the ``-x`` option).
5656

57+
LG_COORDINATOR_TLS
58+
~~~~~~~~~~~~~~~~~~
59+
This variable can be set to enable TLS gRPC channels without using the
60+
``--tls`` option.
61+
5762
LG_PROXY
5863
~~~~~~~~
5964
This variable can be used to specify a SSH proxy hostname which should be used
@@ -128,6 +133,12 @@ Add all resources with the group "example-group" to the place example-place:
128133
129134
$ labgrid-client -p example-place add-match */example-group/*/*
130135
136+
Retrieve a list of places on a ``labgrid-coordinator`` that uses TLS gRPC channels:
137+
138+
.. code-block:: bash
139+
140+
$ labgrid-client --tls [--cert PATH] places
141+
131142
See Also
132143
--------
133144

doc/man/coordinator.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,41 @@ OPTIONS
2525
display command line help
2626
-l ADDRESS, --listen ADDRESS
2727
make coordinator listen on host and port
28+
--tls
29+
enable TLS gRPC channel
30+
--cert
31+
path to TLS certificate (in PEM format)
32+
--key
33+
path to TLS key (in PEM format)
2834
-d, --debug
2935
enable debug mode
3036

37+
TLS WITH A REVERSE PROXY
38+
------------------------
39+
40+
Instead of enabling TLS in ``labgrid-coordinator`` directly, a reverse proxy can
41+
terminate TLS and forward cleartext gRPC to the coordinator. For example, with
42+
``nginx``:
43+
44+
.. code-block:: nginx
45+
46+
server {
47+
listen 20407 ssl http2;
48+
server_name labgrid.example.com;
49+
50+
ssl_certificate /etc/ssl/labgrid-coordinator.crt;
51+
ssl_certificate_key /etc/ssl/labgrid-coordinator.key;
52+
53+
location / {
54+
grpc_pass grpc://127.0.0.1:20408;
55+
}
56+
}
57+
58+
In this setup, start ``labgrid-coordinator`` without ``--tls`` and point
59+
``labgrid-client`` and ``labgrid-exporter`` at the reverse proxy using
60+
``--tls``.
61+
62+
3163
SEE ALSO
3264
--------
3365

doc/man/device-config.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ OPTIONS KEYS
4343
takes as parameter the coordinator ``HOST[:PORT]`` to connect to.
4444
Defaults to ``127.0.0.1:20408``.
4545

46+
``coordinator_tls``
47+
enables a TLS gRPC channel for ``RemotePlace`` connections.
48+
Defaults to whether ``LG_COORDINATOR_TLS`` is set.
49+
50+
``coordinator_cert``
51+
path to the coordinator TLS certificate in PEM format for ``RemotePlace``
52+
connections. If unset, the host CA certificates are used. Relative paths are
53+
resolved relative to the configuration file.
54+
4655
.. _labgrid-device-config-images:
4756

4857
IMAGES

doc/man/exporter.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ OPTIONS
2727
display command line help
2828
-x, --coordinator
2929
coordinator ``HOST[:PORT]`` to connect to, defaults to ``127.0.0.1:20408``
30+
--tls
31+
enable TLS gRPC channel
32+
--cert
33+
path to TLS certificate (in PEM format)
3034
-i, --isolated
3135
enable isolated mode (always request SSH forwards)
3236
-n, --name
@@ -99,6 +103,12 @@ Same as above, but with name ``myname``:
99103
100104
$ labgrid-exporter -n myname my-config.yaml
101105
106+
Same as above, but connecting to a ``labgrid-coordinator`` that uses TLS gRPC channels:
107+
108+
.. code-block:: bash
109+
110+
$ labgrid-exporter --tls [--cert PATH] -n myname my-config.yaml
111+
102112
SEE ALSO
103113
--------
104114

labgrid/remote/client.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
TAG_KEY,
4242
TAG_VAL,
4343
queue_as_aiter,
44+
get_client_credentials,
4445
)
4546
from .. import Environment, Target, target_factory
4647
from ..exceptions import NoDriverFoundError, NoResourceFoundError, InvalidConfigError
@@ -92,6 +93,7 @@ class ClientSession:
9293
the coordinator."""
9394

9495
address = attr.ib(validator=attr.validators.instance_of(str))
96+
credentials = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(grpc.ChannelCredentials)))
9597
loop = attr.ib(validator=attr.validators.instance_of(asyncio.BaseEventLoop))
9698
env = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(Environment)))
9799
role = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(str)))
@@ -120,10 +122,18 @@ def __attrs_post_init__(self):
120122
("grpc.http2.max_pings_without_data", 0), # no limit
121123
]
122124

123-
self.channel = grpc.aio.insecure_channel(
124-
target=self.address,
125-
options=channel_options,
126-
)
125+
if self.credentials:
126+
self.channel = grpc.aio.secure_channel(
127+
target=self.address,
128+
credentials=self.credentials,
129+
options=channel_options,
130+
)
131+
else:
132+
self.channel = grpc.aio.insecure_channel(
133+
target=self.address,
134+
options=channel_options,
135+
)
136+
127137
self.stub = labgrid_coordinator_pb2_grpc.CoordinatorStub(self.channel)
128138

129139
self.out_queue = asyncio.Queue()
@@ -1698,7 +1708,12 @@ def ensure_event_loop(external_loop=None):
16981708

16991709

17001710
def start_session(
1701-
address: str, *, extra: Dict[str, Any] = None, debug: bool = False, loop: "asyncio.AbstractEventLoop | None" = None
1711+
address: str,
1712+
*,
1713+
extra: Dict[str, Any] = None,
1714+
credentials: grpc.ChannelCredentials = None,
1715+
debug: bool = False,
1716+
loop: "asyncio.AbstractEventLoop | None" = None,
17021717
):
17031718
"""
17041719
Starts a ClientSession.
@@ -1720,7 +1735,7 @@ def start_session(
17201735

17211736
address = proxymanager.get_grpc_address(address, default_port=20408)
17221737

1723-
session = ClientSession(address, loop, **extra)
1738+
session = ClientSession(address, credentials, loop, **extra)
17241739
loop.run_until_complete(session.start())
17251740
return session
17261741

@@ -1848,6 +1863,13 @@ def get_parser(auto_doc_mode=False) -> "argparse.ArgumentParser | AutoProgramArg
18481863
type=str,
18491864
help="coordinator HOST[:PORT] (default: value from env variable LG_COORDINATOR, otherwise 127.0.0.1:20408)",
18501865
)
1866+
parser.add_argument(
1867+
"--tls",
1868+
action="store_true",
1869+
default=os.environ.get("LG_COORDINATOR_TLS") is not None,
1870+
help="enable TLS gRPC channel",
1871+
)
1872+
parser.add_argument("--cert", type=pathlib.PurePath, help="path to the server's TLS certificate (in PEM format)")
18511873
parser.add_argument(
18521874
"-c",
18531875
"--config",
@@ -2346,7 +2368,9 @@ def main():
23462368
logging.debug('Starting session with "%s"', coordinator_address)
23472369
loop = asyncio.new_event_loop()
23482370
asyncio.set_event_loop(loop)
2349-
session = start_session(coordinator_address, extra=extra, debug=args.debug, loop=loop)
2371+
session = start_session(
2372+
coordinator_address, extra=extra, credentials=get_client_credentials(args), debug=args.debug, loop=loop
2373+
)
23502374
logging.debug("Started session")
23512375

23522376
try:

labgrid/remote/common.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import re
66
import string
77
import logging
8+
from argparse import Namespace
89
from datetime import datetime
910
from fnmatch import fnmatchcase
10-
11+
from typing import Optional
1112
import attr
13+
import grpc
1214

1315
from .generated import labgrid_coordinator_pb2
1416

@@ -58,6 +60,17 @@ def build_dict_from_map(m):
5860
return d
5961

6062

63+
def get_client_credentials(args: Namespace) -> Optional[grpc.ChannelCredentials]:
64+
if not args.tls:
65+
return None
66+
67+
if not args.cert:
68+
return grpc.ssl_channel_credentials()
69+
70+
with open(args.cert, "rb") as fc:
71+
return grpc.ssl_channel_credentials(fc.read())
72+
73+
6174
@attr.s(eq=False)
6275
class ResourceEntry:
6376
data = attr.ib() # cls, params

labgrid/remote/coordinator.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
import copy
1111
import random
1212
import signal
13-
13+
import pathlib
14+
from typing import Optional
1415
import attr
1516
import grpc
1617
from grpc_reflection.v1alpha import reflection
@@ -1109,7 +1110,7 @@ async def GetReservations(self, request: labgrid_coordinator_pb2.GetReservations
11091110
return labgrid_coordinator_pb2.GetReservationsResponse(reservations=reservations)
11101111

11111112

1112-
async def serve(listen, cleanup) -> None:
1113+
async def serve(listen, cleanup, server_credentials=None) -> None:
11131114
asyncio.current_task().set_name("coordinator-serve")
11141115
# It seems since https://github.com/grpc/grpc/pull/34647, the
11151116
# ping_timeout_ms default of 60 seconds overrides keepalive_timeout_ms,
@@ -1146,7 +1147,11 @@ async def serve(listen, cleanup) -> None:
11461147
except ImportError:
11471148
logging.info("Module grpcio-channelz not available")
11481149

1149-
bound = server.add_insecure_port(listen)
1150+
if server_credentials:
1151+
bound = server.add_secure_port(listen, server_credentials)
1152+
else:
1153+
bound = server.add_insecure_port(listen)
1154+
11501155
logging.debug("Starting server")
11511156
await server.start()
11521157

@@ -1175,6 +1180,18 @@ def callback():
11751180
await server.wait_for_termination()
11761181

11771182

1183+
def get_server_credentials(args: argparse.Namespace) -> Optional[grpc.ServerCredentials]:
1184+
if not args.tls:
1185+
return None
1186+
1187+
if not args.cert or not args.key:
1188+
raise RuntimeError("--cert and --key must be provided when --tls is provided")
1189+
1190+
with open(args.key, "rb") as fk:
1191+
with open(args.cert, "rb") as fc:
1192+
return grpc.ssl_server_credentials([(fk.read(), fc.read())])
1193+
1194+
11781195
def main():
11791196
parser = argparse.ArgumentParser()
11801197
parser.add_argument(
@@ -1185,6 +1202,9 @@ def main():
11851202
default="[::]:20408",
11861203
help="coordinator listening host and port",
11871204
)
1205+
parser.add_argument("--tls", action="store_true", default=False, help="enable TLS gRPC channel")
1206+
parser.add_argument("--cert", type=pathlib.PurePath, help="path to TLS certificate (in PEM format)")
1207+
parser.add_argument("--key", type=pathlib.PurePath, help="path to TLS key (in PEM format)")
11881208
parser.add_argument("-d", "--debug", action="store_true", default=False, help="enable debug mode")
11891209
parser.add_argument("--pystuck", action="store_true", help="enable pystuck")
11901210
parser.add_argument(
@@ -1214,7 +1234,8 @@ def main():
12141234
cleanup = []
12151235
loop.set_debug(True)
12161236
try:
1217-
loop.run_until_complete(serve(args.listen, cleanup))
1237+
server_credentials = get_server_credentials(args)
1238+
loop.run_until_complete(serve(args.listen, cleanup, server_credentials))
12181239
finally:
12191240
if cleanup:
12201241
loop.run_until_complete(*cleanup)

labgrid/remote/exporter.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
from pathlib import Path
1717
from typing import Dict, Type
1818
from socket import gethostname, getfqdn
19+
import pathlib
1920

2021
import attr
2122
import grpc
2223

2324
from .config import ResourceConfig
24-
from .common import ResourceEntry, queue_as_aiter
25+
from .common import ResourceEntry, get_client_credentials, queue_as_aiter
2526
from .generated import labgrid_coordinator_pb2, labgrid_coordinator_pb2_grpc
2627
from ..util import get_free_port, labgrid_version
2728

@@ -831,10 +832,17 @@ def __init__(self, config) -> None:
831832
if urlsplit(f"//{config['coordinator']}").port is None:
832833
config["coordinator"] += ":20408"
833834

834-
self.channel = grpc.aio.insecure_channel(
835-
target=config["coordinator"],
836-
options=channel_options,
837-
)
835+
if config["credentials"]:
836+
self.channel = grpc.aio.secure_channel(
837+
target=config["coordinator"],
838+
credentials=config["credentials"],
839+
options=channel_options,
840+
)
841+
else:
842+
self.channel = grpc.aio.insecure_channel(
843+
target=config["coordinator"],
844+
options=channel_options,
845+
)
838846
self.stub = labgrid_coordinator_pb2_grpc.CoordinatorStub(self.channel)
839847
self.out_queue = asyncio.Queue()
840848
self.pump_task = None
@@ -1081,6 +1089,8 @@ def main():
10811089
default=os.environ.get("LG_COORDINATOR", "127.0.0.1:20408"),
10821090
help="coordinator host and port",
10831091
)
1092+
parser.add_argument("--tls", action="store_true", default=False, help="enable TLS gRPC channel")
1093+
parser.add_argument("--cert", type=pathlib.PurePath, help="path to TLS certificate (in PEM format)")
10841094
parser.add_argument(
10851095
"-n",
10861096
"--name",
@@ -1122,6 +1132,7 @@ def main():
11221132
"hostname": args.hostname or (getfqdn() if args.fqdn else gethostname()),
11231133
"resources": args.resources,
11241134
"coordinator": args.coordinator,
1135+
"credentials": get_client_credentials(args),
11251136
"isolated": args.isolated,
11261137
}
11271138

0 commit comments

Comments
 (0)