Skip to content

Commit ccf7a55

Browse files
feat: Adding ssl support for registry server. (#4718)
1 parent ca3d3c8 commit ccf7a55

File tree

13 files changed

+204
-94
lines changed

13 files changed

+204
-94
lines changed

docs/reference/feature-servers/offline-feature-server.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ helm install feast-offline-server feast-charts/feast-feature-server --set feast_
2323

2424
## Server Example
2525

26-
The complete example can be find under [remote-offline-store-example](../../../examples/remote-offline-store)
26+
The complete example can be found under [remote-offline-store-example](../../../examples/remote-offline-store)
2727

2828
## How to configure the client
2929

docs/reference/feature-servers/python-feature-server.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,12 @@ requests.post(
200200
data=json.dumps(push_data))
201201
```
202202

203-
## Starting the feature server in SSL mode
203+
## Starting the feature server in TLS(SSL) mode
204204

205-
Enabling SSL mode ensures that data between the Feast client and server is transmitted securely. For an ideal production environment, it is recommended to start the feature server in SSL mode.
205+
Enabling TLS mode ensures that data between the Feast client and server is transmitted securely. For an ideal production environment, it is recommended to start the feature server in TLS mode.
206206

207-
### Obtaining a self-signed SSL certificate and key
208-
In development mode we can generate a self-signed certificate for testing. In an actual production environment it is always recommended to get it from a trusted SSL certificate provider.
207+
### Obtaining a self-signed TLS certificate and key
208+
In development mode we can generate a self-signed certificate for testing. In an actual production environment it is always recommended to get it from a trusted TLS certificate provider.
209209

210210
```shell
211211
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
@@ -215,11 +215,11 @@ The above command will generate two files
215215
* `key.pem` : certificate private key
216216
* `cert.pem`: certificate public key
217217

218-
### Starting the Online Server in SSL Mode
219-
To start the feature server in SSL mode, you need to provide the private and public keys using the `--ssl-key-path` and `--ssl-cert-path` arguments with the `feast serve` command.
218+
### Starting the Online Server in TLS(SSL) Mode
219+
To start the feature server in TLS mode, you need to provide the private and public keys using the `--key` and `--cert` arguments with the `feast serve` command.
220220

221221
```shell
222-
feast serve --ssl-key-path key.pem --ssl-cert-path cert.pem
222+
feast serve --key /path/to/key.pem --cert /path/to/cert.pem
223223
```
224224

225225
# Online Feature Server Permissions and Access Control

docs/reference/online-stores/remote.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ provider: local
1616
online_store:
1717
path: http://localhost:6566
1818
type: remote
19-
ssl_cert_path: /path/to/cert.pem
19+
cert: /path/to/cert.pem
2020
entity_key_serialization_version: 2
2121
auth:
2222
type: no_auth
2323
```
2424
{% endcode %}
2525
26-
`ssl_cert_path` is an optional configuration to the public certificate path when the online server starts in SSL mode. This may be needed if the online server is started with a self-signed certificate, typically this file ends with `*.crt`, `*.cer`, or `*.pem`.
26+
`cert` is an optional configuration to the public certificate path when the online server starts in TLS(SSL) mode. This may be needed if the online server is started with a self-signed certificate, typically this file ends with `*.crt`, `*.cer`, or `*.pem`.
2727

2828
## How to configure Authentication and Authorization
2929
Please refer the [page](./../../../docs/getting-started/concepts/permission.md) for more details on how to configure authentication and authorization.

sdk/python/feast/cli.py

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -912,20 +912,22 @@ def init_command(project_directory, minimal: bool, template: str):
912912
show_default=True,
913913
)
914914
@click.option(
915-
"--ssl-key-path",
915+
"--key",
916916
"-k",
917+
"tls_key_path",
917918
type=click.STRING,
918919
default="",
919920
show_default=False,
920-
help="path to SSL certificate private key. You need to pass ssl-cert-path as well to start server in SSL mode",
921+
help="path to TLS certificate private key. You need to pass --cert as well to start server in TLS mode",
921922
)
922923
@click.option(
923-
"--ssl-cert-path",
924+
"--cert",
924925
"-c",
926+
"tls_cert_path",
925927
type=click.STRING,
926928
default="",
927929
show_default=False,
928-
help="path to SSL certificate public key. You need to pass ssl-key-path as well to start server in SSL mode",
930+
help="path to TLS certificate public key. You need to pass --key as well to start server in TLS mode",
929931
)
930932
@click.option(
931933
"--metrics",
@@ -944,14 +946,14 @@ def serve_command(
944946
workers: int,
945947
metrics: bool,
946948
keep_alive_timeout: int,
947-
ssl_key_path: str,
948-
ssl_cert_path: str,
949+
tls_key_path: str,
950+
tls_cert_path: str,
949951
registry_ttl_sec: int = 5,
950952
):
951953
"""Start a feature server locally on a given port."""
952-
if (ssl_key_path and not ssl_cert_path) or (not ssl_key_path and ssl_cert_path):
954+
if (tls_key_path and not tls_cert_path) or (not tls_key_path and tls_cert_path):
953955
raise click.BadParameter(
954-
"Please configure ssl-cert-path and ssl-key-path args to start the feature server in SSL mode."
956+
"Please pass --cert and --key args to start the feature server in TLS mode."
955957
)
956958

957959
store = create_feature_store(ctx)
@@ -964,8 +966,8 @@ def serve_command(
964966
workers=workers,
965967
metrics=metrics,
966968
keep_alive_timeout=keep_alive_timeout,
967-
ssl_key_path=ssl_key_path,
968-
ssl_cert_path=ssl_cert_path,
969+
tls_key_path=tls_key_path,
970+
tls_cert_path=tls_cert_path,
969971
registry_ttl_sec=registry_ttl_sec,
970972
)
971973

@@ -1035,12 +1037,39 @@ def serve_transformations_command(ctx: click.Context, port: int):
10351037
default=DEFAULT_REGISTRY_SERVER_PORT,
10361038
help="Specify a port for the server",
10371039
)
1040+
@click.option(
1041+
"--key",
1042+
"-k",
1043+
"tls_key_path",
1044+
type=click.STRING,
1045+
default="",
1046+
show_default=False,
1047+
help="path to TLS certificate private key. You need to pass --cert as well to start server in TLS mode",
1048+
)
1049+
@click.option(
1050+
"--cert",
1051+
"-c",
1052+
"tls_cert_path",
1053+
type=click.STRING,
1054+
default="",
1055+
show_default=False,
1056+
help="path to TLS certificate public key. You need to pass --key as well to start server in TLS mode",
1057+
)
10381058
@click.pass_context
1039-
def serve_registry_command(ctx: click.Context, port: int):
1059+
def serve_registry_command(
1060+
ctx: click.Context,
1061+
port: int,
1062+
tls_key_path: str,
1063+
tls_cert_path: str,
1064+
):
10401065
"""Start a registry server locally on a given port."""
1066+
if (tls_key_path and not tls_cert_path) or (not tls_key_path and tls_cert_path):
1067+
raise click.BadParameter(
1068+
"Please pass --cert and --key args to start the registry server in TLS mode."
1069+
)
10411070
store = create_feature_store(ctx)
10421071

1043-
store.serve_registry(port)
1072+
store.serve_registry(port, tls_key_path, tls_cert_path)
10441073

10451074

10461075
@cli.command("serve_offline")

sdk/python/feast/feature_server.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -339,10 +339,14 @@ def start_server(
339339
workers: int,
340340
keep_alive_timeout: int,
341341
registry_ttl_sec: int,
342-
ssl_key_path: str,
343-
ssl_cert_path: str,
342+
tls_key_path: str,
343+
tls_cert_path: str,
344344
metrics: bool,
345345
):
346+
if (tls_key_path and not tls_cert_path) or (not tls_key_path and tls_cert_path):
347+
raise ValueError(
348+
"Both key and cert file paths are required to start server in TLS mode."
349+
)
346350
if metrics:
347351
logger.info("Starting Prometheus Server")
348352
start_http_server(8000)
@@ -375,22 +379,22 @@ def start_server(
375379
}
376380

377381
# Add SSL options if the paths exist
378-
if ssl_key_path and ssl_cert_path:
379-
options["keyfile"] = ssl_key_path
380-
options["certfile"] = ssl_cert_path
382+
if tls_key_path and tls_cert_path:
383+
options["keyfile"] = tls_key_path
384+
options["certfile"] = tls_cert_path
381385
FeastServeApplication(store=store, **options).run()
382386
else:
383387
import uvicorn
384388

385389
app = get_app(store, registry_ttl_sec)
386-
if ssl_key_path and ssl_cert_path:
390+
if tls_key_path and tls_cert_path:
387391
uvicorn.run(
388392
app,
389393
host=host,
390394
port=port,
391395
access_log=(not no_access_log),
392-
ssl_keyfile=ssl_key_path,
393-
ssl_certfile=ssl_cert_path,
396+
ssl_keyfile=tls_key_path,
397+
ssl_certfile=tls_cert_path,
394398
)
395399
else:
396400
uvicorn.run(app, host=host, port=port, access_log=(not no_access_log))

sdk/python/feast/feature_store.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1896,8 +1896,8 @@ def serve(
18961896
workers: int = 1,
18971897
metrics: bool = False,
18981898
keep_alive_timeout: int = 30,
1899-
ssl_key_path: str = "",
1900-
ssl_cert_path: str = "",
1899+
tls_key_path: str = "",
1900+
tls_cert_path: str = "",
19011901
registry_ttl_sec: int = 2,
19021902
) -> None:
19031903
"""Start the feature consumption server locally on a given port."""
@@ -1915,8 +1915,8 @@ def serve(
19151915
workers=workers,
19161916
metrics=metrics,
19171917
keep_alive_timeout=keep_alive_timeout,
1918-
ssl_key_path=ssl_key_path,
1919-
ssl_cert_path=ssl_cert_path,
1918+
tls_key_path=tls_key_path,
1919+
tls_cert_path=tls_cert_path,
19201920
registry_ttl_sec=registry_ttl_sec,
19211921
)
19221922

@@ -1949,11 +1949,15 @@ def serve_ui(
19491949
root_path=root_path,
19501950
)
19511951

1952-
def serve_registry(self, port: int) -> None:
1952+
def serve_registry(
1953+
self, port: int, tls_key_path: str = "", tls_cert_path: str = ""
1954+
) -> None:
19531955
"""Start registry server locally on a given port."""
19541956
from feast import registry_server
19551957

1956-
registry_server.start_server(self, port)
1958+
registry_server.start_server(
1959+
self, port=port, tls_key_path=tls_key_path, tls_cert_path=tls_cert_path
1960+
)
19571961

19581962
def serve_offline(self, host: str, port: int) -> None:
19591963
"""Start offline server locally on a given port."""

sdk/python/feast/infra/online_stores/remote.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ class RemoteOnlineStoreConfig(FeastConfigBaseModel):
4141
""" str: Path to metadata store.
4242
If type is 'remote', then this is a URL for registry server """
4343

44-
ssl_cert_path: StrictStr = ""
45-
""" str: Path to the public certificate when the online server starts in SSL mode. This may be needed if the online server started with a self-signed certificate, typically this file ends with `*.crt`, `*.cer`, or `*.pem`.
46-
If type is 'remote', then this configuration is needed to connect to remote online server in SSL mode. """
44+
cert: StrictStr = ""
45+
""" str: Path to the public certificate when the online server starts in TLS(SSL) mode. This may be needed if the online server started with a self-signed certificate, typically this file ends with `*.crt`, `*.cer`, or `*.pem`.
46+
If type is 'remote', then this configuration is needed to connect to remote online server in TLS mode. """
4747

4848

4949
class RemoteOnlineStore(OnlineStore):
@@ -174,11 +174,11 @@ def teardown(
174174
def get_remote_online_features(
175175
session: requests.Session, config: RepoConfig, req_body: str
176176
) -> requests.Response:
177-
if config.online_store.ssl_cert_path:
177+
if config.online_store.cert:
178178
return session.post(
179179
f"{config.online_store.path}/get-online-features",
180180
data=req_body,
181-
verify=config.online_store.ssl_cert_path,
181+
verify=config.online_store.cert,
182182
)
183183
else:
184184
return session.post(

sdk/python/feast/infra/registry/remote.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ class RemoteRegistryConfig(RegistryConfig):
5555
""" str: Path to metadata store.
5656
If registry_type is 'remote', then this is a URL for registry server """
5757

58+
cert: StrictStr = ""
59+
""" str: Path to the public certificate when the registry server starts in TLS(SSL) mode. This may be needed if the registry server started with a self-signed certificate, typically this file ends with `*.crt`, `*.cer`, or `*.pem`.
60+
If registry_type is 'remote', then this configuration is needed to connect to remote registry server in TLS mode. If the remote registry started in non-tls mode then this configuration is not needed."""
61+
5862

5963
class RemoteRegistry(BaseRegistry):
6064
def __init__(
@@ -65,7 +69,17 @@ def __init__(
6569
auth_config: AuthConfig = NoAuthConfig(),
6670
):
6771
self.auth_config = auth_config
68-
self.channel = grpc.insecure_channel(registry_config.path)
72+
assert isinstance(registry_config, RemoteRegistryConfig)
73+
if registry_config.cert:
74+
with open(registry_config.cert, "rb") as cert_file:
75+
trusted_certs = cert_file.read()
76+
tls_credentials = grpc.ssl_channel_credentials(
77+
root_certificates=trusted_certs
78+
)
79+
self.channel = grpc.secure_channel(registry_config.path, tls_credentials)
80+
else:
81+
self.channel = grpc.insecure_channel(registry_config.path)
82+
6983
auth_header_interceptor = GrpcClientAuthHeaderInterceptor(auth_config)
7084
self.channel = grpc.intercept_channel(self.channel, auth_header_interceptor)
7185
self.stub = RegistryServer_pb2_grpc.RegistryServerStub(self.channel)

sdk/python/feast/registry_server.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from concurrent import futures
23
from datetime import datetime, timezone
34
from typing import Optional, Union, cast
@@ -38,6 +39,9 @@
3839
from feast.saved_dataset import SavedDataset, ValidationReference
3940
from feast.stream_feature_view import StreamFeatureView
4041

42+
logger = logging.getLogger(__name__)
43+
logger.setLevel(logging.INFO)
44+
4145

4246
def _build_any_feature_view_proto(feature_view: BaseFeatureView):
4347
if isinstance(feature_view, StreamFeatureView):
@@ -753,7 +757,13 @@ def Proto(self, request, context):
753757
return self.proxied_registry.proto()
754758

755759

756-
def start_server(store: FeatureStore, port: int, wait_for_termination: bool = True):
760+
def start_server(
761+
store: FeatureStore,
762+
port: int,
763+
wait_for_termination: bool = True,
764+
tls_key_path: str = "",
765+
tls_cert_path: str = "",
766+
):
757767
auth_manager_type = str_to_auth_manager_type(store.config.auth_config.type)
758768
init_security_manager(auth_type=auth_manager_type, fs=store)
759769
init_auth_manager(
@@ -781,9 +791,25 @@ def start_server(store: FeatureStore, port: int, wait_for_termination: bool = Tr
781791
)
782792
reflection.enable_server_reflection(service_names_available_for_reflection, server)
783793

784-
server.add_insecure_port(f"[::]:{port}")
794+
if tls_cert_path and tls_key_path:
795+
with open(tls_cert_path, "rb") as cert_file, open(
796+
tls_key_path, "rb"
797+
) as key_file:
798+
certificate_chain = cert_file.read()
799+
private_key = key_file.read()
800+
server_credentials = grpc.ssl_server_credentials(
801+
((private_key, certificate_chain),)
802+
)
803+
logger.info("Starting grpc registry server in TLS(SSL) mode")
804+
server.add_secure_port(f"[::]:{port}", server_credentials)
805+
else:
806+
logger.info("Starting grpc registry server in non-TLS(SSL) mode")
807+
server.add_insecure_port(f"[::]:{port}")
785808
server.start()
786809
if wait_for_termination:
810+
logger.info(
811+
f"Grpc server started at {'https' if tls_cert_path and tls_key_path else 'http'}://localhost:{port}"
812+
)
787813
server.wait_for_termination()
788814
else:
789815
return server

sdk/python/tests/conftest.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
location,
5858
)
5959
from tests.utils.auth_permissions_util import default_store
60+
from tests.utils.generate_self_signed_certifcate_util import generate_self_signed_cert
6061
from tests.utils.http_server import check_port_open, free_port # noqa: E402
6162

6263
logger = logging.getLogger(__name__)
@@ -511,3 +512,19 @@ def auth_config(request, is_integration_test):
511512
return auth_configuration.replace("KEYCLOAK_URL_PLACE_HOLDER", keycloak_url)
512513

513514
return auth_configuration
515+
516+
517+
@pytest.fixture(params=[True, False], scope="module")
518+
def tls_mode(request):
519+
is_tls_mode = request.param
520+
521+
if is_tls_mode:
522+
certificates_path = tempfile.mkdtemp()
523+
tls_key_path = os.path.join(certificates_path, "key.pem")
524+
tls_cert_path = os.path.join(certificates_path, "cert.pem")
525+
generate_self_signed_cert(cert_path=tls_cert_path, key_path=tls_key_path)
526+
else:
527+
tls_key_path = ""
528+
tls_cert_path = ""
529+
530+
return is_tls_mode, tls_key_path, tls_cert_path

0 commit comments

Comments
 (0)