mirror of
https://github.com/penpot/penpot.git
synced 2026-09-01 17:49:08 +00:00
🐳 Add configurable CSP and HSTS headers to the frontend image
Ship both headers from the image so that every deployment starts from a sensible default instead of each installation deriving its own policy. Report-only mode never blocks a request, so this changes no behaviour for existing deployments, and HSTS stays absent unless PENPOT_PUBLIC_URI declares an https scheme. The policy can be narrow because the frontend already reverse proxies its own external dependencies, so 'self' covers them. What it must permit beyond that comes from the code: 'wasm-unsafe-eval' for the render engine, 'unsafe-inline' styles for the inline style attributes of the UI, and blob:/data: for thumbnails, exports and fonts. Closes #11374 Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net> AI-assisted-by: Claude
This commit is contained in:
parent
980ccf15fa
commit
a8ccb5e0cd
@ -27,7 +27,7 @@ COPY ./files/nginx.conf.template /tmp/nginx.conf.template
|
||||
COPY ./files/nginx-resolvers.conf.template /tmp/resolvers.conf.template
|
||||
COPY ./files/nginx-admin-console-locations.conf.template /tmp/nginx-admin-console-locations.conf.template
|
||||
COPY ./files/nginx-mcp-locations.conf.template /tmp/nginx-mcp-locations.conf.template
|
||||
COPY ./files/nginx-security-headers.conf /etc/nginx/nginx-security-headers.conf
|
||||
COPY ./files/nginx-security-headers.conf.template /tmp/nginx-security-headers.conf.template
|
||||
COPY ./files/nginx-mime.types /etc/nginx/mime.types
|
||||
COPY ./files/nginx-external-locations.conf /etc/nginx/overrides/location.d/external-locations.conf
|
||||
COPY ./files/nginx-entrypoint.sh /entrypoint.sh
|
||||
|
||||
@ -84,4 +84,73 @@ export PENPOT_INTERNAL_RESOLVER=${PENPOT_INTERNAL_RESOLVER:-$PENPOT_DEFAULT_INTE
|
||||
envsubst "\$PENPOT_INTERNAL_RESOLVER" \
|
||||
< /tmp/resolvers.conf.template > /etc/nginx/overrides/http.d/resolvers.conf
|
||||
|
||||
#########################################
|
||||
## Security Headers Config
|
||||
#########################################
|
||||
|
||||
# The default policy describes what a stock Penpot deployment actually
|
||||
# needs: 'wasm-unsafe-eval' for the render engine, 'unsafe-inline' styles
|
||||
# for the inline style attributes emitted by the UI, and blob:/data: for
|
||||
# thumbnails, exports and font handling. Everything else is same-origin,
|
||||
# because the Google Fonts and GitHub templates endpoints are reverse
|
||||
# proxied by this very server.
|
||||
#
|
||||
# It ships in report-only mode: the inline <script type="module"> and
|
||||
# <script type="importmap"> blocks of index.html are still reported as
|
||||
# violations, and deployments with plugins enabled additionally report
|
||||
# eval and remote fetch violations from the SES sandbox. Enforcing mode
|
||||
# stays opt-in until both are resolved.
|
||||
export PENPOT_CSP_MODE=${PENPOT_CSP_MODE:-report-only}
|
||||
|
||||
# Remember whether the policy comes from the deployment before the default
|
||||
# is applied, so the warning below only fires for the default one.
|
||||
if [ -n "${PENPOT_CSP_POLICY:-}" ]; then
|
||||
PENPOT_CSP_POLICY_IS_CUSTOM="true"
|
||||
else
|
||||
PENPOT_CSP_POLICY_IS_CUSTOM="false"
|
||||
fi
|
||||
|
||||
export PENPOT_CSP_POLICY=${PENPOT_CSP_POLICY:-"default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' blob: data:; worker-src 'self' blob:; media-src 'self' blob:; frame-src 'self'; manifest-src 'self'"}
|
||||
|
||||
case "${PENPOT_CSP_MODE}" in
|
||||
enforce)
|
||||
export PENPOT_CSP_DIRECTIVE="add_header Content-Security-Policy \"${PENPOT_CSP_POLICY}\" always;"
|
||||
if [ "${PENPOT_CSP_POLICY_IS_CUSTOM}" = "false" ]; then
|
||||
echo "penpot: WARNING: PENPOT_CSP_MODE=enforce is not supported with the default policy yet." >&2
|
||||
echo "penpot: the inline scripts of index.html are not covered by it, so the application will fail to load." >&2
|
||||
echo "penpot: set PENPOT_CSP_POLICY to your own policy, or keep the default report-only mode." >&2
|
||||
fi
|
||||
;;
|
||||
report-only)
|
||||
export PENPOT_CSP_DIRECTIVE="add_header Content-Security-Policy-Report-Only \"${PENPOT_CSP_POLICY}\" always;"
|
||||
;;
|
||||
disabled)
|
||||
export PENPOT_CSP_DIRECTIVE=""
|
||||
;;
|
||||
*)
|
||||
echo "penpot: invalid PENPOT_CSP_MODE '${PENPOT_CSP_MODE}'; expected one of: enforce, report-only, disabled" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# HSTS is only meaningful when the deployment is served over HTTPS, so it
|
||||
# defaults to enabled when PENPOT_PUBLIC_URI declares an https scheme and
|
||||
# to disabled otherwise. Set PENPOT_HSTS_VALUE explicitly to override it,
|
||||
# for example to add includeSubDomains or preload, or to an empty value
|
||||
# to disable it on an https deployment.
|
||||
if [[ "${PENPOT_PUBLIC_URI:-}" == https://* ]]; then
|
||||
export PENPOT_HSTS_VALUE=${PENPOT_HSTS_VALUE-"max-age=31536000"}
|
||||
else
|
||||
export PENPOT_HSTS_VALUE=${PENPOT_HSTS_VALUE-""}
|
||||
fi
|
||||
|
||||
if [ -n "${PENPOT_HSTS_VALUE}" ]; then
|
||||
export PENPOT_HSTS_DIRECTIVE="add_header Strict-Transport-Security \"${PENPOT_HSTS_VALUE}\" always;"
|
||||
else
|
||||
export PENPOT_HSTS_DIRECTIVE=""
|
||||
fi
|
||||
|
||||
envsubst "\$PENPOT_CSP_DIRECTIVE,\$PENPOT_HSTS_DIRECTIVE" \
|
||||
< /tmp/nginx-security-headers.conf.template > /etc/nginx/nginx-security-headers.conf
|
||||
|
||||
exec "$@";
|
||||
|
||||
@ -2,3 +2,5 @@ add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
${PENPOT_CSP_DIRECTIVE}
|
||||
${PENPOT_HSTS_DIRECTIVE}
|
||||
@ -433,6 +433,60 @@ PENPOT_FLAGS: [...] enable-air-gapped-conf
|
||||
When Penpot starts, it will leave out the Nginx configuration related to external requests. This means that,
|
||||
with this flag enabled, the Penpot configuration will disable as well the libraries and templates dashboard and the use of Google fonts.
|
||||
|
||||
## Security headers
|
||||
|
||||
The frontend container always emits `X-Content-Type-Options`, `Referrer-Policy`,
|
||||
`Permissions-Policy` and `X-Frame-Options`. Two additional headers are configurable.
|
||||
|
||||
### Content Security Policy
|
||||
|
||||
Penpot ships a Content Security Policy in **report-only** mode by default. In this mode
|
||||
browsers report violations to the developer console but do not block anything, which makes
|
||||
it safe to enable everywhere while the policy is being tuned.
|
||||
|
||||
```bash
|
||||
PENPOT_CSP_MODE: report-only # report-only (default) | enforce | disabled
|
||||
```
|
||||
|
||||
The default policy is same-origin except for what the application genuinely requires:
|
||||
`'wasm-unsafe-eval'` for the render engine, `'unsafe-inline'` styles for the inline style
|
||||
attributes emitted by the UI, and `blob:`/`data:` for thumbnails, exports and fonts. The
|
||||
external Google Fonts and GitHub templates endpoints do not need entries of their own
|
||||
because they are reverse proxied by the frontend container.
|
||||
|
||||
Two known sources of violations remain, and both are the reason `enforce` is not yet the
|
||||
default:
|
||||
|
||||
- The `index.html` inline `<script type="module">` and `<script type="importmap">` blocks
|
||||
are not covered by the policy yet.
|
||||
- Deployments with plugins enabled report `eval` and remote fetch violations, because the
|
||||
plugin sandbox evaluates third-party code and loads it from arbitrary hosts.
|
||||
|
||||
Set your own policy with `PENPOT_CSP_POLICY` if you need to relax or tighten it, for
|
||||
example to allow plugins:
|
||||
|
||||
```bash
|
||||
PENPOT_CSP_POLICY: "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' https: blob: data:; worker-src 'self' blob:; media-src 'self' blob:; frame-src 'self' https:; manifest-src 'self'"
|
||||
```
|
||||
|
||||
<p class="advice">
|
||||
Because of the above, <code class="language-bash">enforce</code> requires a policy of your
|
||||
own. Enforcing the default policy will prevent the application from loading.
|
||||
</p>
|
||||
|
||||
### HTTP Strict Transport Security
|
||||
|
||||
HSTS is enabled automatically when `PENPOT_PUBLIC_URI` uses the `https` scheme, and
|
||||
disabled otherwise. Override the header value directly to customise it, or set it to an
|
||||
empty value to disable it:
|
||||
|
||||
```bash
|
||||
PENPOT_HSTS_VALUE: "max-age=63072000; includeSubDomains; preload"
|
||||
```
|
||||
|
||||
Note that `includeSubDomains` and `preload` affect every host under your domain and are
|
||||
hard to roll back, so they are not enabled by default.
|
||||
|
||||
## High availability
|
||||
|
||||
The mechanisms for installing Penpot in HA depend largely on how each infrastructure is managed.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user