Skip to content

Commit 80a5b3c

Browse files
feat: Adding SSL support for online server (#4677)
* * Adding the SSL support for online_server. * Adding the SSL support for remote online client. * Adding the integration test to run the remote online server in SSL and non SSL mode. * Incorporated code review comments Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> incorporating code review comments. Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> * Incorporating code review comment. Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> incorporating code review comments. Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> * Update docs/reference/feature-servers/python-feature-server.md Co-authored-by: Francisco Arceo <farceo@redhat.com> Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> * * Update docs/reference/feature-servers/python-feature-server.md * fixing the integration test failure. Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> --------- Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> Co-authored-by: Francisco Arceo <farceo@redhat.com>
1 parent 658b18f commit 80a5b3c

File tree

9 files changed

+248
-37
lines changed

9 files changed

+248
-37
lines changed

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

+22
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,28 @@ requests.post(
200200
data=json.dumps(push_data))
201201
```
202202

203+
## Starting the feature server in SSL mode
204+
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.
206+
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.
209+
210+
```shell
211+
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
212+
```
213+
214+
The above command will generate two files
215+
* `key.pem` : certificate private key
216+
* `cert.pem`: certificate public key
217+
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.
220+
221+
```shell
222+
feast serve --ssl-key-path key.pem --ssl-cert-path cert.pem
223+
```
224+
203225
# Online Feature Server Permissions and Access Control
204226

205227
## API Endpoints and Permissions

docs/reference/online-stores/remote.md

+3
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ provider: local
1616
online_store:
1717
path: http://localhost:6566
1818
type: remote
19+
ssl_cert_path: /path/to/cert.pem
1920
entity_key_serialization_version: 2
2021
auth:
2122
type: no_auth
2223
```
2324
{% endcode %}
2425
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`.
27+
2528
## How to configure Authentication and Authorization
2629
Please refer the [page](./../../../docs/getting-started/concepts/permission.md) for more details on how to configure authentication and authorization.
2730

sdk/python/feast/cli.py

+25
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,22 @@ def init_command(project_directory, minimal: bool, template: str):
911911
default=5,
912912
show_default=True,
913913
)
914+
@click.option(
915+
"--ssl-key-path",
916+
"-k",
917+
type=click.STRING,
918+
default="",
919+
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+
)
922+
@click.option(
923+
"--ssl-cert-path",
924+
"-c",
925+
type=click.STRING,
926+
default="",
927+
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",
929+
)
914930
@click.option(
915931
"--metrics",
916932
"-m",
@@ -928,9 +944,16 @@ def serve_command(
928944
workers: int,
929945
metrics: bool,
930946
keep_alive_timeout: int,
947+
ssl_key_path: str,
948+
ssl_cert_path: str,
931949
registry_ttl_sec: int = 5,
932950
):
933951
"""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):
953+
raise click.BadParameter(
954+
"Please configure ssl-cert-path and ssl-key-path args to start the feature server in SSL mode."
955+
)
956+
934957
store = create_feature_store(ctx)
935958

936959
store.serve(
@@ -941,6 +964,8 @@ def serve_command(
941964
workers=workers,
942965
metrics=metrics,
943966
keep_alive_timeout=keep_alive_timeout,
967+
ssl_key_path=ssl_key_path,
968+
ssl_cert_path=ssl_cert_path,
944969
registry_ttl_sec=registry_ttl_sec,
945970
)
946971

sdk/python/feast/feature_server.py

+26-9
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,8 @@ 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,
342344
metrics: bool,
343345
):
344346
if metrics:
@@ -364,16 +366,31 @@ def start_server(
364366
logger.debug("Auth manager initialized successfully")
365367

366368
if sys.platform != "win32":
367-
FeastServeApplication(
368-
store=store,
369-
bind=f"{host}:{port}",
370-
accesslog=None if no_access_log else "-",
371-
workers=workers,
372-
keepalive=keep_alive_timeout,
373-
registry_ttl_sec=registry_ttl_sec,
374-
).run()
369+
options = {
370+
"bind": f"{host}:{port}",
371+
"accesslog": None if no_access_log else "-",
372+
"workers": workers,
373+
"keepalive": keep_alive_timeout,
374+
"registry_ttl_sec": registry_ttl_sec,
375+
}
376+
377+
# 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
381+
FeastServeApplication(store=store, **options).run()
375382
else:
376383
import uvicorn
377384

378385
app = get_app(store, registry_ttl_sec)
379-
uvicorn.run(app, host=host, port=port, access_log=(not no_access_log))
386+
if ssl_key_path and ssl_cert_path:
387+
uvicorn.run(
388+
app,
389+
host=host,
390+
port=port,
391+
access_log=(not no_access_log),
392+
ssl_keyfile=ssl_key_path,
393+
ssl_certfile=ssl_cert_path,
394+
)
395+
else:
396+
uvicorn.run(app, host=host, port=port, access_log=(not no_access_log))

sdk/python/feast/feature_store.py

+4
Original file line numberDiff line numberDiff line change
@@ -1896,6 +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 = "",
18991901
registry_ttl_sec: int = 2,
19001902
) -> None:
19011903
"""Start the feature consumption server locally on a given port."""
@@ -1913,6 +1915,8 @@ def serve(
19131915
workers=workers,
19141916
metrics=metrics,
19151917
keep_alive_timeout=keep_alive_timeout,
1918+
ssl_key_path=ssl_key_path,
1919+
ssl_cert_path=ssl_cert_path,
19161920
registry_ttl_sec=registry_ttl_sec,
19171921
)
19181922

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

+14-3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ 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. """
47+
4448

4549
class RemoteOnlineStore(OnlineStore):
4650
"""
@@ -170,6 +174,13 @@ def teardown(
170174
def get_remote_online_features(
171175
session: requests.Session, config: RepoConfig, req_body: str
172176
) -> requests.Response:
173-
return session.post(
174-
f"{config.online_store.path}/get-online-features", data=req_body
175-
)
177+
if config.online_store.ssl_cert_path:
178+
return session.post(
179+
f"{config.online_store.path}/get-online-features",
180+
data=req_body,
181+
verify=config.online_store.ssl_cert_path,
182+
)
183+
else:
184+
return session.post(
185+
f"{config.online_store.path}/get-online-features", data=req_body
186+
)

sdk/python/tests/integration/online_store/test_remote_online_store.py

+60-23
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
start_feature_server,
1616
)
1717
from tests.utils.cli_repo_creator import CliRunner
18+
from tests.utils.generate_self_signed_certifcate_util import generate_self_signed_cert
1819
from tests.utils.http_server import free_port
1920

2021

22+
@pytest.mark.parametrize("ssl_mode", [True, False])
2123
@pytest.mark.integration
22-
def test_remote_online_store_read(auth_config):
24+
def test_remote_online_store_read(auth_config, ssl_mode):
2325
with tempfile.TemporaryDirectory() as remote_server_tmp_dir, tempfile.TemporaryDirectory() as remote_client_tmp_dir:
2426
permissions_list = [
2527
Permission(
@@ -41,11 +43,12 @@ def test_remote_online_store_read(auth_config):
4143
actions=[AuthzedAction.READ_ONLINE],
4244
),
4345
]
44-
server_store, server_url, registry_path = (
46+
server_store, server_url, registry_path, ssl_cert_path = (
4547
_create_server_store_spin_feature_server(
4648
temp_dir=remote_server_tmp_dir,
4749
auth_config=auth_config,
4850
permissions_list=permissions_list,
51+
ssl_mode=ssl_mode,
4952
)
5053
)
5154
assert None not in (server_store, server_url, registry_path)
@@ -54,6 +57,7 @@ def test_remote_online_store_read(auth_config):
5457
server_registry_path=str(registry_path),
5558
feature_server_url=server_url,
5659
auth_config=auth_config,
60+
ssl_cert_path=ssl_cert_path,
5761
)
5862
assert client_store is not None
5963
_assert_non_existing_entity_feature_views_entity(
@@ -159,21 +163,46 @@ def _assert_client_server_online_stores_are_matching(
159163

160164

161165
def _create_server_store_spin_feature_server(
162-
temp_dir, auth_config: str, permissions_list
166+
temp_dir, auth_config: str, permissions_list, ssl_mode: bool
163167
):
164168
store = default_store(str(temp_dir), auth_config, permissions_list)
165169
feast_server_port = free_port()
170+
if ssl_mode:
171+
certificates_path = tempfile.mkdtemp()
172+
ssl_key_path = os.path.join(certificates_path, "key.pem")
173+
ssl_cert_path = os.path.join(certificates_path, "cert.pem")
174+
generate_self_signed_cert(cert_path=ssl_cert_path, key_path=ssl_key_path)
175+
else:
176+
ssl_key_path = ""
177+
ssl_cert_path = ""
178+
166179
server_url = next(
167180
start_feature_server(
168-
repo_path=str(store.repo_path), server_port=feast_server_port
181+
repo_path=str(store.repo_path),
182+
server_port=feast_server_port,
183+
ssl_key_path=ssl_key_path,
184+
ssl_cert_path=ssl_cert_path,
169185
)
170186
)
171-
print(f"Server started successfully, {server_url}")
172-
return store, server_url, os.path.join(store.repo_path, "data", "registry.db")
187+
if ssl_cert_path and ssl_key_path:
188+
print(f"Online Server started successfully in SSL mode, {server_url}")
189+
else:
190+
print(f"Server started successfully, {server_url}")
191+
192+
return (
193+
store,
194+
server_url,
195+
os.path.join(store.repo_path, "data", "registry.db"),
196+
ssl_cert_path,
197+
)
173198

174199

175200
def _create_remote_client_feature_store(
176-
temp_dir, server_registry_path: str, feature_server_url: str, auth_config: str
201+
temp_dir,
202+
server_registry_path: str,
203+
feature_server_url: str,
204+
auth_config: str,
205+
ssl_cert_path: str = "",
177206
) -> FeatureStore:
178207
project_name = "REMOTE_ONLINE_CLIENT_PROJECT"
179208
runner = CliRunner()
@@ -185,27 +214,35 @@ def _create_remote_client_feature_store(
185214
registry_path=server_registry_path,
186215
feature_server_url=feature_server_url,
187216
auth_config=auth_config,
217+
ssl_cert_path=ssl_cert_path,
188218
)
189219

190220
return FeatureStore(repo_path=repo_path)
191221

192222

193223
def _overwrite_remote_client_feature_store_yaml(
194-
repo_path: str, registry_path: str, feature_server_url: str, auth_config: str
224+
repo_path: str,
225+
registry_path: str,
226+
feature_server_url: str,
227+
auth_config: str,
228+
ssl_cert_path: str = "",
195229
):
196230
repo_config = os.path.join(repo_path, "feature_store.yaml")
197-
with open(repo_config, "w") as repo_config:
198-
repo_config.write(
199-
dedent(
200-
f"""
201-
project: {PROJECT_NAME}
202-
registry: {registry_path}
203-
provider: local
204-
online_store:
205-
path: {feature_server_url}
206-
type: remote
207-
entity_key_serialization_version: 2
208-
"""
209-
)
210-
+ auth_config
211-
)
231+
232+
config_content = "entity_key_serialization_version: 2\n" + auth_config
233+
config_content += dedent(
234+
f"""
235+
project: {PROJECT_NAME}
236+
registry: {registry_path}
237+
provider: local
238+
online_store:
239+
path: {feature_server_url}
240+
type: remote
241+
"""
242+
)
243+
244+
if ssl_cert_path:
245+
config_content += f" ssl_cert_path: {ssl_cert_path}\n"
246+
247+
with open(repo_config, "w") as repo_config_file:
248+
repo_config_file.write(config_content)

sdk/python/tests/utils/auth_permissions_util.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@ def default_store(
5454
return fs
5555

5656

57-
def start_feature_server(repo_path: str, server_port: int, metrics: bool = False):
57+
def start_feature_server(
58+
repo_path: str,
59+
server_port: int,
60+
metrics: bool = False,
61+
ssl_key_path: str = "",
62+
ssl_cert_path: str = "",
63+
):
5864
host = "0.0.0.0"
5965
cmd = [
6066
"feast",
@@ -65,6 +71,13 @@ def start_feature_server(repo_path: str, server_port: int, metrics: bool = False
6571
"--port",
6672
str(server_port),
6773
]
74+
75+
if ssl_cert_path and ssl_cert_path:
76+
cmd.append("--ssl-key-path")
77+
cmd.append(ssl_key_path)
78+
cmd.append("--ssl-cert-path")
79+
cmd.append(ssl_cert_path)
80+
6881
feast_server_process = subprocess.Popen(
6982
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
7083
)
@@ -91,7 +104,13 @@ def start_feature_server(repo_path: str, server_port: int, metrics: bool = False
91104
"localhost", 8000
92105
), "Prometheus server is running when it should be disabled."
93106

94-
yield f"http://localhost:{server_port}"
107+
online_server_url = (
108+
f"https://localhost:{server_port}"
109+
if ssl_key_path and ssl_cert_path
110+
else f"http://localhost:{server_port}"
111+
)
112+
113+
yield (online_server_url)
95114

96115
if feast_server_process is not None:
97116
feast_server_process.kill()

0 commit comments

Comments
 (0)