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

Add security headers to the ZenML server #2583

Merged
merged 7 commits into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker/base.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ RUN groupadd --gid $USER_GID $USERNAME \
RUN mkdir -p /zenml/.zenconfig/local_stores/default_zen_store && chown -R $USER_UID:$USER_GID /zenml
ENV PATH="$PATH:/home/$USERNAME/.local/bin"

ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug"]
ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug", "--no-server-header"]
CMD ["--port", "8080", "--host", "0.0.0.0"]
2 changes: 1 addition & 1 deletion docker/zenml-server-dev.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ ENV ZENML_CONFIG_PATH=/zenml/.zenconfig \
ZENML_DEBUG=true \
ZENML_ANALYTICS_OPT_IN=false

ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug"]
ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug", "--no-server-header"]
CMD ["--port", "8080", "--host", "0.0.0.0"]
2 changes: 1 addition & 1 deletion docker/zenml-server-hf-spaces.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ ENV ZENML_SERVER_DEPLOYMENT_TYPE="hf_spaces"
# ENV ZENML_SECRETS_STORE_VAULT_NAMESPACE=""
# ENV ZENML_SECRETS_STORE_MAX_VERSIONS=""

ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug"]
ENTRYPOINT ["uvicorn", "zenml.zen_server.zen_server_api:app", "--log-level", "debug", "--no-server-header"]
CMD ["--port", "8080", "--host", "0.0.0.0"]
18 changes: 18 additions & 0 deletions docs/book/deploying-zenml/zenml-self-hosted/deploy-with-docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,24 @@ These configuration options are not required for most use cases, but can be usef
openssl rand -hex 32
```

The environment variables starting with **ZENML\_SERVER\_SECURE*\_HEADERS\_** can be used to enable, disable or set custom values for security headers in the ZenML server's HTTP responses. The following values can be set for any of the supported secure headers configuration options:

* `enabled`, `on`, `true` or `yes` - enables the secure header with the default value.
* `disabled`, `off`, `false`, `none` or `no` - disables the secure header entirely, so that it is not set in the ZenML server's HTTP responses.
* any other value - sets the secure header to the specified value.

The following secure headers environment variables are supported:

* **ZENML\_SERVER\_SECURE*\_HEADERS\_SERVER**: The `Server` HTTP header value used to identify the server. The default value is the ZenML server ID.
* **ZENML\_SERVER\_SECURE\_HEADERS\_HSTS**: The `Strict-Transport-Security` HTTP header value. The default value is `max-age=63072000; includeSubDomains`.
* **ZENML\_SERVER\_SECURE\_HEADERS\_XFO**: The `X-Frame-Options` HTTP header value. The default value is `SAMEORIGIN`.
* **ZENML\_SERVER\_SECURE\_HEADERS\_XXP**: The `X-XSS-Protection` HTTP header value. The default value is `0`. NOTE: this header is deprecated and should not be customized anymore. The `Content-Security-Policy` header should be used instead.
* **ZENML\_SERVER\_SECURE\_HEADERS\_CONTENT**: The `X-Content-Type-Options` HTTP header value. The default value is `nosniff`.
* **ZENML\_SERVER\_SECURE\_HEADERS\_CSP**: The `Content-Security-Policy` HTTP header value. This is by default set to a strict CSP policy that only allows content from the origins required by the ZenML dashboard. NOTE: customizing this header is discouraged, as it may cause the ZenML dashboard to malfunction.
* **ZENML\_SERVER\_SECURE\_HEADERS\_REFERRER**: The `Referrer-Policy` HTTP header value. The default value is `no-referrer-when-downgrade`.
* **ZENML\_SERVER\_SECURE\_HEADERS\_CACHE**: The `Cache-Control` HTTP header value. The default value is `no-store, no-cache, must-revalidate`.
* **ZENML\_SERVER\_SECURE\_HEADERS\_PERMISSIONS**: The `Permissions-Policy` HTTP header value. The default value is `accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()`.

## Run the ZenML server with Docker

As previously mentioned, the ZenML server container image uses sensible defaults for most configuration options. This means that you can simply run the container with Docker without any additional configuration and it will work out of the box for most use cases:
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ fastapi-utils = { version = "~0.2.1", optional = true }
orjson = { version = "~3.10.0", optional = true }
Jinja2 = { version = "*", optional = true }
ipinfo = { version = ">=4.4.3", optional = true }
secure = { version = "~0.3.0", optional = true }

# Optional dependencies for project templates
copier = { version = ">=8.1.0", optional = true }
Expand Down Expand Up @@ -181,6 +182,7 @@ server = [
"orjson",
"Jinja2",
"ipinfo",
"secure",
]
templates = ["copier", "jinja2-time", "ruff", "pyyaml-include"]
terraform = ["python-terraform"]
Expand Down
116 changes: 115 additions & 1 deletion src/zenml/config/server_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import json
import os
from secrets import token_hex
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union
from uuid import UUID

from pydantic import BaseModel, Field, SecretStr, root_validator
Expand All @@ -30,6 +30,14 @@
DEFAULT_ZENML_SERVER_LOGIN_RATE_LIMIT_MINUTE,
DEFAULT_ZENML_SERVER_MAX_DEVICE_AUTH_ATTEMPTS,
DEFAULT_ZENML_SERVER_PIPELINE_RUN_AUTH_WINDOW,
DEFAULT_ZENML_SERVER_SECURE_HEADERS_CACHE,
DEFAULT_ZENML_SERVER_SECURE_HEADERS_CONTENT,
DEFAULT_ZENML_SERVER_SECURE_HEADERS_CSP,
DEFAULT_ZENML_SERVER_SECURE_HEADERS_HSTS,
DEFAULT_ZENML_SERVER_SECURE_HEADERS_PERMISSIONS,
DEFAULT_ZENML_SERVER_SECURE_HEADERS_REFERRER,
DEFAULT_ZENML_SERVER_SECURE_HEADERS_XFO,
DEFAULT_ZENML_SERVER_SECURE_HEADERS_XXP,
ENV_ZENML_SERVER_PREFIX,
)
from zenml.enums import AuthScheme
Expand Down Expand Up @@ -128,6 +136,76 @@ class ServerConfiguration(BaseModel):
server.
login_rate_limit_minute: The number of login attempts allowed per minute.
login_rate_limit_day: The number of login attempts allowed per day.
secure_headers_server: Custom value to be set in the `Server` HTTP
header to identify the server. If not specified, or if set to one of
the reserved values `enabled`, `yes`, `true`, `on`, the `Server`
header will be set to the default value (ZenML server ID). If set to
one of the reserved values `disabled`, `no`, `none`, `false`, `off`
or to an empty string, the `Server` header will not be included in
responses.
secure_headers_hsts: The server header value to be set in the HTTP
header `Strict-Transport-Security`. If not specified, or if set to
one of the reserved values `enabled`, `yes`, `true`, `on`, the
`Strict-Transport-Security` header will be set to the default value
(`max-age=63072000; includeSubdomains`). If set to one of
the reserved values `disabled`, `no`, `none`, `false`, `off` or to
an empty string, the `Strict-Transport-Security` header will not be
included in responses.
secure_headers_xfo: The server header value to be set in the HTTP
header `X-Frame-Options`. If not specified, or if set to one of the
reserved values `enabled`, `yes`, `true`, `on`, the `X-Frame-Options`
header will be set to the default value (`SAMEORIGIN`). If set to
one of the reserved values `disabled`, `no`, `none`, `false`, `off`
or to an empty string, the `X-Frame-Options` header will not be
included in responses.
secure_headers_xxp: The server header value to be set in the HTTP
header `X-XSS-Protection`. If not specified, or if set to one of the
reserved values `enabled`, `yes`, `true`, `on`, the `X-XSS-Protection`
header will be set to the default value (`0`). If set to one of the
reserved values `disabled`, `no`, `none`, `false`, `off` or
to an empty string, the `X-XSS-Protection` header will not be
included in responses. NOTE: this header is deprecated and should
always be set to `0`. The `Content-Security-Policy` header should be
used instead.
secure_headers_content: The server header value to be set in the HTTP
header `X-Content-Type-Options`. If not specified, or if set to one
of the reserved values `enabled`, `yes`, `true`, `on`, the
`X-Content-Type-Options` header will be set to the default value
(`nosniff`). If set to one of the reserved values `disabled`, `no`,
`none`, `false`, `off` or to an empty string, the
`X-Content-Type-Options` header will not be included in responses.
secure_headers_csp: The server header value to be set in the HTTP
header `Content-Security-Policy`. If not specified, or if set to one
of the reserved values `enabled`, `yes`, `true`, `on`, the
`Content-Security-Policy` header will be set to a default value
that is compatible with the ZenML dashboard. If set to one of the
reserved values `disabled`, `no`, `none`, `false`, `off` or to an
empty string, the `Content-Security-Policy` header will not be
included in responses.
secure_headers_referrer: The server header value to be set in the HTTP
header `Referrer-Policy`. If not specified, or if set to one of the
reserved values `enabled`, `yes`, `true`, `on`, the `Referrer-Policy`
header will be set to the default value
(`no-referrer-when-downgrade`). If set to one of the reserved values
`disabled`, `no`, `none`, `false`, `off` or to an empty string, the
`Referrer-Policy` header will not be included in responses.
secure_headers_cache: The server header value to be set in the HTTP
header `Cache-Control`. If not specified, or if set to one of the
reserved values `enabled`, `yes`, `true`, `on`, the `Cache-Control`
header will be set to the default value
(`no-store, no-cache, must-revalidate`). If set to one of the
reserved values `disabled`, `no`, `none`, `false`, `off` or to an
empty string, the `Cache-Control` header will not be included in
responses.
secure_headers_permissions: The server header value to be set in the
HTTP header `Permissions-Policy`. If not specified, or if set to one
of the reserved values `enabled`, `yes`, `true`, `on`, the
`Permissions-Policy` header will be set to the default value
(`accelerometer=(), camera=(), geolocation=(), gyroscope=(),
magnetometer=(), microphone=(), payment=(), usb=()`). If set to
one of the reserved values `disabled`, `no`, `none`, `false`, `off`
or to an empty string, the `Permissions-Policy` header will not be
included in responses.
"""

deployment_type: ServerDeploymentType = ServerDeploymentType.OTHER
Expand Down Expand Up @@ -171,6 +249,32 @@ class ServerConfiguration(BaseModel):
login_rate_limit_minute: int = DEFAULT_ZENML_SERVER_LOGIN_RATE_LIMIT_MINUTE
login_rate_limit_day: int = DEFAULT_ZENML_SERVER_LOGIN_RATE_LIMIT_DAY

secure_headers_server: Union[bool, str] = True
secure_headers_hsts: Union[bool, str] = (
DEFAULT_ZENML_SERVER_SECURE_HEADERS_HSTS
)
secure_headers_xfo: Union[bool, str] = (
DEFAULT_ZENML_SERVER_SECURE_HEADERS_XFO
)
secure_headers_xxp: Union[bool, str] = (
DEFAULT_ZENML_SERVER_SECURE_HEADERS_XXP
)
secure_headers_content: Union[bool, str] = (
DEFAULT_ZENML_SERVER_SECURE_HEADERS_CONTENT
)
secure_headers_csp: Union[bool, str] = (
DEFAULT_ZENML_SERVER_SECURE_HEADERS_CSP
)
secure_headers_referrer: Union[bool, str] = (
DEFAULT_ZENML_SERVER_SECURE_HEADERS_REFERRER
)
secure_headers_cache: Union[bool, str] = (
DEFAULT_ZENML_SERVER_SECURE_HEADERS_CACHE
)
secure_headers_permissions: Union[bool, str] = (
DEFAULT_ZENML_SERVER_SECURE_HEADERS_PERMISSIONS
)

_deployment_id: Optional[UUID] = None

@root_validator(pre=True)
Expand Down Expand Up @@ -221,6 +325,16 @@ def _validate_config(cls, values: Dict[str, Any]) -> Dict[str, Any]:
f"The server metadata is not a valid JSON string: {e}"
)

# if one of the secure headers options is set to a boolean value, set
# the corresponding value
for k, v in values.copy().items():
if k.startswith("secure_headers_") and isinstance(v, str):
if v.lower() in ["disabled", "no", "none", "false", "off", ""]:
values[k] = False
if v.lower() in ["enabled", "yes", "true", "on"]:
# Revert to the default value if the header is enabled
del values[k]

return values

@property
Expand Down
28 changes: 28 additions & 0 deletions src/zenml/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,34 @@ def handle_int_env_var(var: str, default: int = 0) -> int:
DEFAULT_ZENML_SERVER_LOGIN_RATE_LIMIT_MINUTE = 5
DEFAULT_ZENML_SERVER_LOGIN_RATE_LIMIT_DAY = 1000

DEFAULT_ZENML_SERVER_SECURE_HEADERS_HSTS = (
"max-age=63072000; includeSubdomains"
)
DEFAULT_ZENML_SERVER_SECURE_HEADERS_XFO = "SAMEORIGIN"
DEFAULT_ZENML_SERVER_SECURE_HEADERS_XXP = "0"
DEFAULT_ZENML_SERVER_SECURE_HEADERS_CONTENT = "nosniff"
DEFAULT_ZENML_SERVER_SECURE_HEADERS_CSP = (
"default-src 'none'; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
"connect-src 'self' https://sdkdocs.zenml.io https://hubapi.zenml.io; "
"img-src 'self' data: https://public-flavor-logos.s3.eu-central-1.amazonaws.com; "
"style-src 'self' 'unsafe-inline'; "
"base-uri 'self'; "
"form-action 'self'; "
"font-src 'self';"
"frame-src https://zenml.hellonext.co https://sdkdocs.zenml.io "
)
DEFAULT_ZENML_SERVER_SECURE_HEADERS_REFERRER = "no-referrer-when-downgrade"
DEFAULT_ZENML_SERVER_SECURE_HEADERS_CACHE = (
"no-store, no-cache, must-revalidate"
)
DEFAULT_ZENML_SERVER_SECURE_HEADERS_PERMISSIONS = (
"accelerometer=(), autoplay=(), camera=(), encrypted-media=(), "
"geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), "
"payment=(), sync-xhr=(), usb=()"
)
DEFAULT_ZENML_SERVER_SECURE_HEADERS_REPORT_TO = "default"

# Configurations to decide which resources report their usage and check for
# entitlement in the case of a cloud deployment. Expected Format is this:
# ENV_ZENML_REPORTABLE_RESOURCES='["Foo", "bar"]'
Expand Down
1 change: 1 addition & 0 deletions src/zenml/zen_server/deploy/docker/docker_zen_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def run(self) -> None:
host="0.0.0.0", # nosec
port=self.endpoint.config.port or 8000,
log_level="info",
server_header=False,
)
except KeyboardInterrupt:
logger.info("ZenML Server stopped. Resuming normal execution.")
3 changes: 3 additions & 0 deletions src/zenml/zen_server/deploy/helm/templates/_environment.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ base_url: {{ .ZenML.baseURL | quote }}
{{- if .ZenML.auth.rbacImplementationSource }}
rbac_implementation_source: {{ .ZenML.auth.rbacImplementationSource | quote }}
{{- end }}
{{- range $key, $value := .ZenML.secure_headers }}
secure_headers_{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}


Expand Down
43 changes: 43 additions & 0 deletions src/zenml/zen_server/deploy/helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,49 @@ zenml:
# variable in the `zenml.secretEnvironment` variable.
class_path: my.custom.secrets.store.MyCustomSecretsStore

# The ZenML server's secure headers configuration. This can be used to
# enable, disable or set custom values for security headers in the ZenML
# server's HTTP responses. The following values can be set for any of the
# supported secure headers configuration options:
#
# - `enabled`, `on`, `true` or `yes` - enables the secure header with the
# default value.
# - `disabled`, `off`, `false`, `none` or `no` - disables the secure header
# entirely, so that it is not set in the ZenML server's HTTP responses.
# - any other value - sets the secure header to the specified value.
secure_headers:
# The `Server` HTTP header value used to identify the server. The default
# value is the ZenML server ID.
server: enabled
# The `Strict-Transport-Security` HTTP header value. The default value is
# `max-age=63072000; includeSubDomains`.
hsts: enabled
# The `X-Frame-Options` HTTP header value. The default value is `SAMEORIGIN`.
xfo: enabled
# The `X-XSS-Protection` HTTP header value. The default value is `0`.
# NOTE: this header is deprecated and should not be customized anymore. The
# `Content-Security-Policy` header should be used instead.
xxp: enabled
# The `X-Content-Type-Options` HTTP header value. The default value is
# `nosniff`.
content: enabled
# The `Content-Security-Policy` HTTP header value. This is by default set
# to a strict CSP policy that only allows content from the origins required
# by the ZenML dashboard.
# NOTE: customizing this header is discouraged, as it may cause the ZenML
# dashboard to malfunction.
csp: enabled
# The `Referrer-Policy` HTTP header value. The default value is
# `no-referrer-when-downgrade`.
referrer: enabled
# The `Cache-Control` HTTP header value. The default value is
# `no-store, no-cache, must-revalidate`.
cache: enabled
# The `Permissions-Policy` HTTP header value. The default value is
# `accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()`.
permissions: enabled


# Extra environment variables to set in the ZenML server container.
environment: {}

Expand Down
1 change: 1 addition & 0 deletions src/zenml/zen_server/deploy/local/local_zen_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ def run(self) -> None:
host=self.endpoint.config.ip_address,
port=self.endpoint.config.port or 8000,
log_level="info",
server_header=False,
)
except KeyboardInterrupt:
logger.info("ZenML Server stopped. Resuming normal execution.")