Skip to content

Commit aa01cd0

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 a5adaa5 commit aa01cd0

12 files changed

Lines changed: 410 additions & 19 deletions

doc/getting_started.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,21 @@ 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 secure gRPC channels in a production environment.
442+
443+
This can be enabled on the ``labgrid-coordinator`` by adding the ``--secure``,
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 secure gRPC channels enabled you need to pass
449+
the ``--secure`` (and ``--cert`` if the certificate is not trusted by the host
450+
machine) option.
451+
Refer to the ``labgrid-client`` and ``labgrid-exporter`` man pages for details.
452+
438453
Using a Strategy
439454
----------------
440455

doc/man/client.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ Add all resources with the group "example-group" to the place example-place:
128128
129129
$ labgrid-client -p example-place add-match */example-group/*/*
130130
131+
Retrieve a list of places on a ``labgrid-coordinator`` that uses secure gRPC channels:
132+
133+
.. code-block:: bash
134+
135+
$ labgrid-client --secure [--cert PATH] places
136+
131137
See Also
132138
--------
133139

doc/man/coordinator.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ OPTIONS
2525
display command line help
2626
-l ADDRESS, --listen ADDRESS
2727
make coordinator listen on host and port
28+
--secure
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

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+
--secure
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 secure gRPC channels:
107+
108+
.. code-block:: bash
109+
110+
$ labgrid-exporter --secure [--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
@@ -93,6 +94,7 @@ class ClientSession:
9394
the coordinator."""
9495

9596
address = attr.ib(validator=attr.validators.instance_of(str))
97+
credentials = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(grpc.ChannelCredentials)))
9698
loop = attr.ib(validator=attr.validators.instance_of(asyncio.BaseEventLoop))
9799
env = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(Environment)))
98100
role = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(str)))
@@ -121,10 +123,18 @@ def __attrs_post_init__(self):
121123
("grpc.http2.max_pings_without_data", 0), # no limit
122124
]
123125

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

130140
self.out_queue = asyncio.Queue()
@@ -1793,7 +1803,12 @@ def ensure_event_loop(external_loop=None):
17931803

17941804

17951805
def start_session(
1796-
address: str, *, extra: Dict[str, Any] = None, debug: bool = False, loop: "asyncio.AbstractEventLoop | None" = None
1806+
address: str,
1807+
*,
1808+
extra: Dict[str, Any] = None,
1809+
credentials: grpc.ChannelCredentials = None,
1810+
debug: bool = False,
1811+
loop: "asyncio.AbstractEventLoop | None" = None,
17971812
):
17981813
"""
17991814
Starts a ClientSession.
@@ -1815,7 +1830,7 @@ def start_session(
18151830

18161831
address = proxymanager.get_grpc_address(address, default_port=20408)
18171832

1818-
session = ClientSession(address, loop, **extra)
1833+
session = ClientSession(address, credentials, loop, **extra)
18191834
loop.run_until_complete(session.start())
18201835
return session
18211836

@@ -1943,6 +1958,13 @@ def get_parser(auto_doc_mode=False) -> "argparse.ArgumentParser | AutoProgramArg
19431958
type=str,
19441959
help="coordinator HOST[:PORT] (default: value from env variable LG_COORDINATOR, otherwise 127.0.0.1:20408)",
19451960
)
1961+
parser.add_argument(
1962+
"--secure",
1963+
action="store_true",
1964+
default=os.environ.get("LG_COORDINATOR_SECURE") is not None,
1965+
help="enable TLS gRPC channel",
1966+
)
1967+
parser.add_argument("--cert", type=pathlib.PurePath, help="path to the server's TLS certificate (in PEM format)")
19461968
parser.add_argument(
19471969
"-c",
19481970
"--config",
@@ -2453,7 +2475,9 @@ def main():
24532475
logging.debug('Starting session with "%s"', coordinator_address)
24542476
loop = asyncio.new_event_loop()
24552477
asyncio.set_event_loop(loop)
2456-
session = start_session(coordinator_address, extra=extra, debug=args.debug, loop=loop)
2478+
session = start_session(
2479+
coordinator_address, extra=extra, credentials=get_client_credentials(args), debug=args.debug, loop=loop
2480+
)
24572481
logging.debug("Started session")
24582482

24592483
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.secure:
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
@@ -1107,7 +1108,7 @@ async def GetReservations(self, request: labgrid_coordinator_pb2.GetReservations
11071108
return labgrid_coordinator_pb2.GetReservationsResponse(reservations=reservations)
11081109

11091110

1110-
async def serve(listen, cleanup) -> None:
1111+
async def serve(listen, cleanup, server_credentials=None) -> None:
11111112
asyncio.current_task().set_name("coordinator-serve")
11121113
# It seems since https://github.com/grpc/grpc/pull/34647, the
11131114
# ping_timeout_ms default of 60 seconds overrides keepalive_timeout_ms,
@@ -1144,7 +1145,11 @@ async def serve(listen, cleanup) -> None:
11441145
except ImportError:
11451146
logging.info("Module grpcio-channelz not available")
11461147

1147-
bound = server.add_insecure_port(listen)
1148+
if server_credentials:
1149+
bound = server.add_secure_port(listen, server_credentials)
1150+
else:
1151+
bound = server.add_insecure_port(listen)
1152+
11481153
logging.debug("Starting server")
11491154
await server.start()
11501155

@@ -1173,6 +1178,18 @@ def callback():
11731178
await server.wait_for_termination()
11741179

11751180

1181+
def get_server_credentials(args: argparse.Namespace) -> Optional[grpc.ServerCredentials]:
1182+
if not args.secure:
1183+
return None
1184+
1185+
if not args.cert or not args.key:
1186+
raise RuntimeError("--cert and --key must be provided when --secure is provided")
1187+
1188+
with open(args.key, "rb") as fk:
1189+
with open(args.cert, "rb") as fc:
1190+
return grpc.ssl_server_credentials([(fk.read(), fc.read())])
1191+
1192+
11761193
def main():
11771194
parser = argparse.ArgumentParser()
11781195
parser.add_argument(
@@ -1183,6 +1200,9 @@ def main():
11831200
default="[::]:20408",
11841201
help="coordinator listening host and port",
11851202
)
1203+
parser.add_argument("--secure", action="store_true", default=False, help="enable TLS to secure the gRPC channel")
1204+
parser.add_argument("--cert", type=pathlib.PurePath, help="path to TLS certificate (in PEM format)")
1205+
parser.add_argument("--key", type=pathlib.PurePath, help="path to TLS key (in PEM format)")
11861206
parser.add_argument("-d", "--debug", action="store_true", default=False, help="enable debug mode")
11871207
parser.add_argument("--pystuck", action="store_true", help="enable pystuck")
11881208
parser.add_argument(
@@ -1212,7 +1232,8 @@ def main():
12121232
cleanup = []
12131233
loop.set_debug(True)
12141234
try:
1215-
loop.run_until_complete(serve(args.listen, cleanup))
1235+
server_credentials = get_server_credentials(args)
1236+
loop.run_until_complete(serve(args.listen, cleanup, server_credentials))
12161237
finally:
12171238
if cleanup:
12181239
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

@@ -909,10 +910,17 @@ def __init__(self, config) -> None:
909910
if urlsplit(f"//{config['coordinator']}").port is None:
910911
config["coordinator"] += ":20408"
911912

912-
self.channel = grpc.aio.insecure_channel(
913-
target=config["coordinator"],
914-
options=channel_options,
915-
)
913+
if config["credentials"]:
914+
self.channel = grpc.aio.secure_channel(
915+
target=config["coordinator"],
916+
credentials=config["credentials"],
917+
options=channel_options,
918+
)
919+
else:
920+
self.channel = grpc.aio.insecure_channel(
921+
target=config["coordinator"],
922+
options=channel_options,
923+
)
916924
self.stub = labgrid_coordinator_pb2_grpc.CoordinatorStub(self.channel)
917925
self.out_queue = asyncio.Queue()
918926
self.pump_task = None
@@ -1159,6 +1167,8 @@ def main():
11591167
default=os.environ.get("LG_COORDINATOR", "127.0.0.1:20408"),
11601168
help="coordinator host and port",
11611169
)
1170+
parser.add_argument("--secure", action="store_true", default=False, help="enable TLS to secure the gRPC channel")
1171+
parser.add_argument("--cert", type=pathlib.PurePath, help="path to TLS certificate (in PEM format)")
11621172
parser.add_argument(
11631173
"-n",
11641174
"--name",
@@ -1200,6 +1210,7 @@ def main():
12001210
"hostname": args.hostname or (getfqdn() if args.fqdn else gethostname()),
12011211
"resources": args.resources,
12021212
"coordinator": args.coordinator,
1213+
"credentials": get_client_credentials(args),
12031214
"isolated": args.isolated,
12041215
}
12051216

man/labgrid-client.1

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ This is the client to control a boards status and interface with it on remote ma
3939
.INDENT 3.5
4040
.sp
4141
.EX
42-
usage: labgrid\-client [\-x ADDRESS] [\-c CONFIG] [\-p PLACE] [\-s STATE]
43-
[\-i INITIAL_STATE] [\-d] [\-v] [\-P PROXY]
42+
usage: labgrid\-client [\-x ADDRESS] [\-\-secure] [\-\-cert CERT] [\-c CONFIG]
43+
[\-p PLACE] [\-s STATE] [\-i INITIAL_STATE] [\-d] [\-v]
44+
[\-P PROXY]
4445
COMMAND ...
4546
.EE
4647
.UNINDENT
@@ -52,6 +53,16 @@ coordinator HOST[:PORT] (default: value from env variable LG_COORDINATOR, otherw
5253
.UNINDENT
5354
.INDENT 0.0
5455
.TP
56+
.B \-\-secure
57+
enable TLS gRPC channel
58+
.UNINDENT
59+
.INDENT 0.0
60+
.TP
61+
.B \-\-cert <cert>
62+
path to the server\(aqs TLS certificate (in PEM format)
63+
.UNINDENT
64+
.INDENT 0.0
65+
.TP
5566
.B \-c <config>, \-\-config <config>
5667
env config file (default: value from env variable LG_ENV)
5768
.UNINDENT
@@ -1267,6 +1278,16 @@ $ labgrid\-client \-p example\-place add\-match */example\-group/*/*
12671278
.EE
12681279
.UNINDENT
12691280
.UNINDENT
1281+
.sp
1282+
Retrieve a list of places on a \fBlabgrid\-coordinator\fP that uses secure gRPC channels:
1283+
.INDENT 0.0
1284+
.INDENT 3.5
1285+
.sp
1286+
.EX
1287+
$ labgrid\-client \-\-secure [\-\-cert PATH] places
1288+
.EE
1289+
.UNINDENT
1290+
.UNINDENT
12701291
.SH SEE ALSO
12711292
.sp
12721293
\fBlabgrid\-exporter\fP(1)

man/labgrid-coordinator.1

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ display command line help
5151
.BI \-l \ ADDRESS\fR,\fB \ \-\-listen \ ADDRESS
5252
make coordinator listen on host and port
5353
.TP
54+
.B \-\-secure
55+
enable TLS gRPC channel
56+
.TP
57+
.B \-\-cert
58+
path to TLS certificate (in PEM format)
59+
.TP
60+
.B \-\-key
61+
path to TLS key (in PEM format)
62+
.TP
5463
.B \-d\fP,\fB \-\-debug
5564
enable debug mode
5665
.UNINDENT

0 commit comments

Comments
 (0)