Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gRPC authentication proposal - using gRPC secure channels #1541

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2033967
gRPC authentication proposal - using gRPC secure channels
jacbar01-arm Nov 12, 2024
cd90a83
adding faciliation to use authentication plugins delivered as a separ…
jacbar01-arm Nov 20, 2024
1af5927
gRPC authentication - updating the documentation
jacbar01-arm Nov 20, 2024
6dfdb3c
updating usage.rst description file
jacbar01-arm Nov 20, 2024
a497284
gRPC documentation - minor updates
jacbar01-arm Nov 20, 2024
6e25285
Merge branch 'master' into master
JacekBartynowski Nov 20, 2024
eb2d3c8
driver/rawnetworkinterfacedriver: add interface up/down/wait_state
Bastian-Krause Jul 5, 2024
6317b7e
driver/rawnetworkinterfacedriver: ethtool get/change settings
Bastian-Krause Oct 15, 2024
63c1cf3
driver/rawnetworkinterfacedriver: ethtool get/change EEE settings
Bastian-Krause Oct 10, 2024
d167ef1
driver/rawnetworkinterfacedriver: ethtool get/change pause settings
Bastian-Krause Oct 10, 2024
18f6d47
gRPC authentication proposal - using gRPC secure channels
jacbar01-arm Nov 12, 2024
2e932e4
adding faciliation to use authentication plugins delivered as a separ…
jacbar01-arm Nov 20, 2024
b24d1ae
gRPC authentication - updating the documentation
jacbar01-arm Nov 20, 2024
109b361
updating usage.rst description file
jacbar01-arm Nov 20, 2024
36adf03
gRPC documentation - minor updates
jacbar01-arm Nov 20, 2024
e42ce8c
Merge branch 'master' of github.com:JacekBartynowski/labgrid
jacbar01-arm Nov 20, 2024
e4d6c00
Merge branch 'master' of github.com:JacekBartynowski/labgrid
jacbar01-arm Nov 20, 2024
7faf458
Merge branch 'master' of github.com:JacekBartynowski/labgrid
jacbar01-arm Nov 20, 2024
43c26b3
gRPC authentication - fixing issues detected by ruff
jacbar01-arm Nov 20, 2024
3284161
gRPC authentication - adding instructions on SSL certificate and key …
jacbar01-arm Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/development.rst
Original file line number Diff line number Diff line change
Expand Up @@ -778,3 +778,11 @@ processes well.
An implementation should start a new process,
return a handle and forbid running other processes in the foreground.
The handle can be used to retrieve output from a command.

gRPC Authentication
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The gRPC authentication procedure is based on the mechanism presented in the
official gRPC repository. It utilizes:
- SSL certificates and keys to secure the gRPC channels
- plugins intended to encode and decode authentication information using HTTP/2 headers
42 changes: 42 additions & 0 deletions doc/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -872,3 +872,45 @@ like this:
$ labgrid-client -p example allow sirius/john

To remove the allow it is currently necessary to unlock and lock the place.

Enabling the gRPC connection authentication
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

To enable the gRPC connection (with the labgrid coordinator) authentication, we need to run the labgrid-client and
labgrid-exporter with the -A option.
The gRPC authentication procedure requires:
- providing a path to the SSL certificate file to encrypt the gRPC channel
- providing an instance of the client authentication plugin class (class dervied from grpc.AuthMetadataPlugin)
By default, the provided ``certificates/server.crt`` SSL certificate file is used to encrypt the gRPC channel and
``DefaultAuthMetadataPlugin`` plugin is used to update the HTTP/2 headers with authentication information on the client side.

Labgrid coordinator supports the gRPC authentication when the -A command line option is specified.
The labgrid-coordinator, by default uses the provided ``DefaultServerInterceptor`` plugin to validate the authentication
information passed using HTTP/2 headers, for the gRPC channel encryption the coordinator, by default used provided SSL
certificate file: ``certificates/server.crt`` and key file: ``certificates/server.key``.

Generation of SSL certificates for gRPC authentication purposes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The repository contains certificate file: ``certificates/server.crt``, key file: ``certificates/server.key`` and sample
configuration file: ``certificates/cert.conf`` that can be used to re-generate the certificate and key.

Please check and modify the configuration file: ``certificates/cert.conf`` according to your needs, and ensure that
all of your supported addresses, hostnames, and domains are listed in the ``alt_names`` section.

generate certificate and key using the following command run inside the ``certificate`` directory:

.. code-block:: bash

$ openssl req -config cert.conf -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout server.key -out server.crt

please notice that the certificate is valid for 365 days, consider shortening or extending this period

verify the generated certificate using the following command run inside the ``certificate`` directory:

.. code-block:: bash

$ openssl x509 -noout -text -in server.crt

ensure that all of your
supported addresses, hostnames, and domains are present
2 changes: 2 additions & 0 deletions labgrid/remote/authentication/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.pyc
__pycache__/*
10 changes: 10 additions & 0 deletions labgrid/remote/authentication/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
__all__ = ['load_certificate_from_file', 'get_auth_meta_plugin', 'get_server_interceptor',
'DEFAULT_CERTIFICATE_PATH', 'DEFAULT_KEY_PATH']

from .helper_functions import (
DEFAULT_CERTIFICATE_PATH,
DEFAULT_KEY_PATH,
get_auth_meta_plugin,
get_server_interceptor,
load_certificate_from_file,
)
116 changes: 116 additions & 0 deletions labgrid/remote/authentication/helper_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import logging
import os

import attr
import grpc
import pkg_resources

from .plugins_interceptors import DefaultAuthMetadataPlugin, DefaultServerInterceptor

DEFAULT_CERTIFICATE_PATH = "../certificates/server.crt"
DEFAULT_KEY_PATH = "../certificates/server.key"

@attr.s(eq=False)
class AuthenticationPluginError(Exception):
msg = attr.ib(validator=attr.validators.instance_of(str))


def load_certificate_from_file(filepath):
'''
Loads certificate from file and returns it as bytes

:raises FileNotFoundError: when a file with certificate cannot be found

Args:
filepath (str): The path to the file to load

Returns:
The content of the certificate file as bytes
'''
if filepath in (DEFAULT_CERTIFICATE_PATH, DEFAULT_KEY_PATH):
logging.warn('Using default self-signed certificate or certificate key')

real_path = os.path.join(os.path.dirname(__file__), filepath)

if not os.path.exists(real_path):
raise FileNotFoundError(f"File {real_path} not found")

with open(real_path, "rb") as f:
return f.read()


def get_auth_meta_plugin(plugin_name):
'''
Returns an instance of the grpc.AuthMetadataPlugin class specified by name
passed as an input parameter.
The plugin should be available via installed Python package.

It is also possible to use the default plugin: DefaultAuthMetadataPlugin when
the value of the input argument is 'default.

:raises AuthenticationPluginError: when the plugin does not meet certain requirements

Args:
plugin_name (str): name of the authentication plugin used for the gRPC
channel authentication/authorization purposes

Returns:
Instance of the grpc.AuthMetadataPlugin class
'''
instance = None

if plugin_name != "default":

for entry_point in pkg_resources.iter_entry_points('auth_plugin'):
if entry_point.name == plugin_name:
auth_metadata_plugin = entry_point.load()
instance = auth_metadata_plugin()
break

else:
instance = DefaultAuthMetadataPlugin()

if not isinstance(instance, grpc.AuthMetadataPlugin):
raise AuthenticationPluginError(f'Plugin: {plugin_name}'
' is not of grpc.AuthMetadataPlugin type')

if not callable(instance):
raise AuthenticationPluginError(f'Plugin: {plugin_name}'
' does not implement __call__ method')

return instance


def get_server_interceptor(interceptor_name):
'''
Returns an instance of the grpc.ServerInterceptor class specified by name passed as an input parameter.

The server interceptor should be available via installed Python package.

It is also possible to use the default interceptor: DefaultServerInterceptor when
the value of the input argument is 'default'.

:raises AuthenticationPluginError: when the interceptor does not meet certain requirements
'''
instance = None

if interceptor_name != "default":

for entry_point in pkg_resources.iter_entry_points('server_interceptor'):
if entry_point.name == interceptor_name:
interceptor = entry_point.load()
instance = interceptor()
return instance

else:
instance = DefaultServerInterceptor()

if not isinstance(instance, grpc.aio.ServerInterceptor):
raise AuthenticationPluginError(f'Interceptor: {interceptor_name}'
' is not of grpc.aio.ServerInterceptor type')

if not hasattr(instance, 'intercept_service'):
raise AuthenticationPluginError(f'Interceptor: {interceptor_name}'
' does not implement intercept_servcice method')

return instance
38 changes: 38 additions & 0 deletions labgrid/remote/authentication/plugins_interceptors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import grpc

token_value = 'authorized'

class DefaultAuthMetadataPlugin(grpc.AuthMetadataPlugin):
'''
Authentication plugin used to add {'authorization', "Bearer <token_value>"} HTTP header
'''
def __call__(self, context, callback):
callback((("authorization", f"Bearer {token_value}"),), None)


class DefaultServerInterceptor(grpc.aio.ServerInterceptor):
'''
Middleware used to validate the JWT token in the HTTP header
'''
def __init__(self):
def abort(ignored_request, context):
context.abort(grpc.StatusCode.UNAUTHENTICATED, "Invalid signature")
self._abort_handler = grpc.unary_unary_rpc_method_handler(abort)

def intercept_service(self, continuation, handler_call_details):
'''
Extracts the token from the HTTP header and validates it
'''
token = ''
for item in handler_call_details.invocation_metadata:
dictionary = item._asdict()
if dictionary['key'] == 'authorization':
token = dictionary['value']
if "Bearer " in token:
token = token.replace("Bearer ", "")
break

if token != '' and token == token_value:
return continuation(handler_call_details)

self._abort_handler()
27 changes: 27 additions & 0 deletions labgrid/remote/certificates/cert.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[ req ]
default_bits = 2048
prompt = no
distinguished_name = dn
x509_extensions = req_ext
default_md = sha256

[ dn ]
C = GB
ST = SomeState
L = SomeCity
O = SomeCompany
OU = SomeDepartment
CN = localhost
emailAddress = [email protected]

[ req_ext ]
keyUsage = critical, digitalSignature, keyAgreement
basicConstraints=CA:true
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = some-company.com
DNS.2 = localhost
DNS.3 = 127.0.0.1
IP.1 = 127.0.0.1
IP.2 = ::1
25 changes: 25 additions & 0 deletions labgrid/remote/certificates/server.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEPTCCAyWgAwIBAgIUP8SVoseFfTESZc4c9SfjDpuoKWwwDQYJKoZIhvcNAQEL
BQAwgaExCzAJBgNVBAYTAkdCMRIwEAYDVQQIDAlTb21lU3RhdGUxETAPBgNVBAcM
CFNvbWVDaXR5MRQwEgYDVQQKDAtTb21lQ29tcGFueTEXMBUGA1UECwwOU29tZURl
cGFydG1lbnQxEjAQBgNVBAMMCWxvY2FsaG9zdDEoMCYGCSqGSIb3DQEJARYZZW1h
aWwuYWRkcmVzc0Bjb21wYW55LmNvbTAeFw0yNDExMjYwODEzNDlaFw0zNDExMjQw
ODEzNDlaMIGhMQswCQYDVQQGEwJHQjESMBAGA1UECAwJU29tZVN0YXRlMREwDwYD
VQQHDAhTb21lQ2l0eTEUMBIGA1UECgwLU29tZUNvbXBhbnkxFzAVBgNVBAsMDlNv
bWVEZXBhcnRtZW50MRIwEAYDVQQDDAlsb2NhbGhvc3QxKDAmBgkqhkiG9w0BCQEW
GWVtYWlsLmFkZHJlc3NAY29tcGFueS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQDlLcILCQQt+CWEUGRV6ESXcqnfntgw9pR7sGAOahBjgMjDlSZf
mS9XWWLwob4L8+wQkyClyW75D4w+wV+xPMFsk8u0lala7Ig10x93ha5BTXS3ASNC
zzVoT1R/8WF4kC54oIKv3fUchZKMFiaWbgdvwUXFhLMLOmMMFcOpDSofepmNTS++
WS9vIVXvOYwqHFaN6MuH3LnET0b0NQNH+CRWhVXDPrYZ01hJ0tD/IiqUHKwpClN/
rD0Y08Ob8wjtXGlqywNsqPJlZp21M2X9sQPgw2XEhYDOes1YdBNY1RxDRZzzObsm
eTo5zkIlNccMm6eUkJzKLzfptCC3GEYMYyKnAgMBAAGjazBpMA4GA1UdDwEB/wQE
AwIDiDAMBgNVHRMEBTADAQH/MEkGA1UdEQRCMECCEHNvbWUtY29tcGFueS5jb22C
CWxvY2FsaG9zdIIJMTI3LjAuMC4xhwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0G
CSqGSIb3DQEBCwUAA4IBAQAAT2tF9iBWLRiAEEEtp9NvAHHK/OKQzn+v5/SEU3cD
MPW0t3NzRFg8bHfQBiKrdK7ViWO/taBXDdkMdOESkYavI4RD9y2w+iPz++TdQlM2
BCTUDV5uTE1K7m+yP0BK3y0QwS8wl9j/paT5qpfC2zZI2ZAhk5Q4m1EU7FlUHmeJ
4MpJbAS7Fa2hamSJchMjbR38J8JoiO6t6v11+btSB91+aJA9IIaqTXegEaYYle2y
hneEZL/L2j3jL0CS/SETQ8y5X1B1FUn0WfvksK/f+INWLOgEyz6iT2vSZXawZpK2
qvWXeXdWodfZ3a4v+/RTqebSnK3sJz2RWjqQE51xdfxJ
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions labgrid/remote/certificates/server.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDlLcILCQQt+CWE
UGRV6ESXcqnfntgw9pR7sGAOahBjgMjDlSZfmS9XWWLwob4L8+wQkyClyW75D4w+
wV+xPMFsk8u0lala7Ig10x93ha5BTXS3ASNCzzVoT1R/8WF4kC54oIKv3fUchZKM
FiaWbgdvwUXFhLMLOmMMFcOpDSofepmNTS++WS9vIVXvOYwqHFaN6MuH3LnET0b0
NQNH+CRWhVXDPrYZ01hJ0tD/IiqUHKwpClN/rD0Y08Ob8wjtXGlqywNsqPJlZp21
M2X9sQPgw2XEhYDOes1YdBNY1RxDRZzzObsmeTo5zkIlNccMm6eUkJzKLzfptCC3
GEYMYyKnAgMBAAECggEBANTR8D8FKW9y34wGh5ZLMd8d6OgzfvnBEdPmztyz2/I6
9rTBPbhK7W6FIF7rJCu/YPlnV0r9SuNYb9kbA7z3+XrVqLZSwMkhm7+4BaGdb1tP
DVKgaNkyyJrMCGEji2tFIRJ9o76jNGo/E/6o1z6cgKVj6mHov8dueeoQD/ldsz80
fR0Yzdzp+55NNsqmvUFvlDumt25yzIRV2m6yqqz39Zl/B9pXYjXdVTkdnqcO7HLE
FsG8+X5l584D/M1gvz2KTSmitl5V8pfoWdaDGUqsx3TW3uJwKV4pjC7TAFx5CLYT
It+8qIusP4VxTFIKC07dd0HCy8GOXuobwUK4t3n28FkCgYEA/jvkmcr2zx+lh/2l
rJYJbuuHj+cSvaRrbgPdy74m4rEaUHkU1280zb1N7HIFN777Xlfs3Ojn+eCg5ekZ
N69QnIbM8UTe6bIxupoKEoXq1NcbAGX+I3pHgnnc8q/F+6gXHBHBVqr0P/KGqasz
NPo4MDkxolhS2rvYB/QHDWgojDUCgYEA5sVPHasrL3ff5XvGH7nYHfBDKSPbTHm6
zl2fqsvkzjtmf0zeB4xBQSg89qtzpTzREn+Q3bZqDOGJ1K/YWujgPVq3Np3fBS4K
CYLygixH4ntnI2tYeeS9BLZ9UfzEAxoKTZEd1cGmcGjI5gRLnTGtUXoYO2271nhO
ijdYmpGidusCgYEAkyTBE44YORrc6I+S0wfnn84sIMqh2ycNkpgkR+bfhLbyPv9F
Y8cWbbmSHzaC9JfRzvHewqD+mm47UbYPBV6vrliKx12QEvwysgizqbLejp+NHjbp
10jPmKHFkqTPVu8bqQBRwUKiqVxKOms+8Pudh7OimY67LaQozbmcV1MgnL0CgYEA
2r/P1hTKE+3yy4p/bNVymjaEwzudBiohvLqcvn8V/bPq3eLUWJ1HwebNmxk8vzYe
DQXlIXVno5wrVfP2B37WCPKz107g9/0DQK7jCCfHYFWPl9CKhskfr5b4xj3u4+3M
NGjJujUde1KolPkfX/uWCjTNQZAsTQmvkW8TSzmfok8CgYAfkN+EW0i3vlnNnF8O
GojO3INcTK+mfVD6fyCAeWLkW6UaOvN8cFERz/HIBJ5O/KSEQG6RVztqWNZtxK5U
uc6gYcomphiMXiZ5e8nbDPdoAlUZD2a8InTfLhuf0Ih3l9IhvN06MLGZ4t984Eus
XFwArBYXp4js3j1L+5eXDH6e5A==
-----END PRIVATE KEY-----
27 changes: 22 additions & 5 deletions labgrid/remote/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
)
from .. import Environment, Target, target_factory
from ..exceptions import NoDriverFoundError, NoResourceFoundError, InvalidConfigError
from .authentication import load_certificate_from_file, get_auth_meta_plugin, DEFAULT_CERTIFICATE_PATH
from .generated import labgrid_coordinator_pb2, labgrid_coordinator_pb2_grpc
from ..resource.remote import RemotePlaceManager, RemotePlace
from ..util import diff_dict, flat_dict, dump, atomic_replace, labgrid_version, Timeout
Expand Down Expand Up @@ -99,10 +100,22 @@ def __attrs_post_init__(self):
("grpc.http2.max_pings_without_data", 0), # no limit
]

self.channel = grpc.aio.insecure_channel(
target=self.address,
options=channel_options,
)
if self.args.auth:
call_credentials = grpc.metadata_call_credentials(get_auth_meta_plugin(self.args.auth_plugin),
name=self.args.auth_plugin)
channel_credentials = grpc.ssl_channel_credentials(load_certificate_from_file(self.args.cert_path))
composite_credentials = grpc.composite_channel_credentials(channel_credentials, call_credentials)

self.channel = grpc.aio.secure_channel(
target=self.address,
credentials=composite_credentials,
options=channel_options,
)
else:
self.channel = grpc.aio.insecure_channel(
target=self.address,
options=channel_options,
)
self.stub = labgrid_coordinator_pb2_grpc.CoordinatorStub(self.channel)

self.out_queue = asyncio.Queue()
Expand Down Expand Up @@ -1699,12 +1712,16 @@ def main():
)
parser.add_argument("-v", "--verbose", action="count", default=0)
parser.add_argument("-P", "--proxy", type=str, help="proxy connections via given ssh host")
parser.add_argument("-A", "--auth", action="store_true", default=False, help="enable gRPC authentication")
parser.add_argument("-cp", "--cert-path", type=str, default=DEFAULT_CERTIFICATE_PATH,
help="path to file with SSL certificate to secrure gRPC channel")
parser.add_argument("-ap", "--auth-plugin", type=str, default="default",
help="name of the plugin used for the authentication purposes")
subparsers = parser.add_subparsers(
dest="command",
title="available subcommands",
metavar="COMMAND",
)

subparser = subparsers.add_parser("help")

subparser = subparsers.add_parser("complete")
Expand Down
Loading