mirror of
https://github.com/penpot/penpot.git
synced 2026-09-18 18:06:14 +00:00
The frontend build now emits the sha256 hashes of the inline scripts of every page it writes into resources/public, the image moves them out of the document root, and the entrypoint splices them into the default script-src. This removes one of the two reasons why enforcing mode was not usable. The hashes are computed on the rendered output rather than on the mustache templates, since the digest covers the exact bytes served between the script tags. All four served pages contribute, not just index.html: challenge.html handles the redirect, render.html is loaded by the exporter in a headless browser, and rasterizer.html is initialised by the frontend itself, so leaving any of them out would have broken those paths under enforcing mode. The storybook previews are excluded because that container does not serve them. A bundle predating this change yields no hashes and the policy stays as it was, so older bundles keep building. The three external locations were also passing through the security headers of their upstreams. raw.githubusercontent.com returns its own Content-Security-Policy and both it and fonts.googleapis.com return Strict-Transport-Security. Browsers enforce the intersection of every policy they receive, so the upstream one takes precedence on those responses, and the HSTS one lands on our own host, meaning a deployment that deliberately disables HSTS would get it set anyway by a third party. Hide all three at the proxy. Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
166 lines
6.7 KiB
Bash
166 lines
6.7 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
is_truthy() {
|
|
local value="${1,,}"
|
|
[[ "$value" == "true" || "$value" == "t" || "$value" == "1" ]]
|
|
}
|
|
|
|
is_falsy() {
|
|
local value="${1,,}"
|
|
[[ "$value" == "false" || "$value" == "f" || "$value" == "0" ]]
|
|
}
|
|
|
|
|
|
#########################################
|
|
## Air Gapped config
|
|
#########################################
|
|
|
|
if [[ $PENPOT_FLAGS == *"enable-air-gapped-conf"* ]]; then
|
|
rm /etc/nginx/overrides/location.d/external-locations.conf;
|
|
export PENPOT_FLAGS="$PENPOT_FLAGS disable-google-fonts-provider disable-dashboard-templates-section"
|
|
fi
|
|
|
|
#########################################
|
|
## App Frontend config
|
|
#########################################
|
|
|
|
update_flags() {
|
|
if [ -n "$PENPOT_FLAGS" ]; then
|
|
echo "$(sed \
|
|
-e "s|^//var penpotFlags = .*;|var penpotFlags = \"$PENPOT_FLAGS\";|g" \
|
|
"$1")" > "$1"
|
|
fi
|
|
|
|
if [ -n "$PENPOT_PUBLIC_URI" ]; then
|
|
echo "var penpotPublicURI = \"$PENPOT_PUBLIC_URI\";" >> "$1";
|
|
fi
|
|
}
|
|
|
|
update_oidc_name() {
|
|
if [ -n "$PENPOT_OIDC_NAME" ]; then
|
|
echo "$(sed \
|
|
-e "s|^//var penpotOIDCName = .*;|var penpotOIDCName = \"$PENPOT_OIDC_NAME\";|g" \
|
|
"$1")" > "$1"
|
|
fi
|
|
}
|
|
|
|
update_flags /var/www/app/js/config.js
|
|
update_oidc_name /var/www/app/js/config.js
|
|
|
|
#########################################
|
|
## Nginx Config
|
|
#########################################
|
|
|
|
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
|
|
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
|
|
export PENPOT_HTTP_SERVER_MAX_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_BODY_SIZE:-367001600} # Default to 350MiB
|
|
export PENPOT_IPV6_LISTEN_DIRECTIVE=${PENPOT_IPV6_LISTEN_DIRECTIVE:-"listen [::]:8080 default_server reuseport backlog=16384;"}
|
|
if is_truthy "${PENPOT_DISABLE_IPV6_LISTEN:-}"; then
|
|
export PENPOT_IPV6_LISTEN_DIRECTIVE=""
|
|
fi
|
|
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE,\$PENPOT_IPV6_LISTEN_DIRECTIVE" \
|
|
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
|
|
|
|
if [[ $PENPOT_FLAGS == *"enable-admin-console"* ]]; then
|
|
export PENPOT_ADMIN_CONSOLE_URI=${PENPOT_ADMIN_CONSOLE_URI:-http://penpot-admin-console:3000}
|
|
envsubst "\$PENPOT_ADMIN_CONSOLE_URI" \
|
|
< /tmp/nginx-admin-console-locations.conf.template > /etc/nginx/overrides/server.d/admin-console-locations.conf
|
|
else
|
|
rm -f /etc/nginx/overrides/server.d/admin-console-locations.conf
|
|
fi
|
|
|
|
if [[ $PENPOT_FLAGS == *"enable-mcp"* ]]; then
|
|
export PENPOT_MCP_URI=${PENPOT_MCP_URI:-http://penpot-mcp:4401}
|
|
export PENPOT_MCP_URI_WS=${PENPOT_MCP_URI_WS:-http://penpot-mcp:4402}
|
|
|
|
envsubst "\$PENPOT_MCP_URI,\$PENPOT_MCP_URI_WS" \
|
|
< /tmp/nginx-mcp-locations.conf.template > /etc/nginx/overrides/server.d/mcp-locations.conf
|
|
else
|
|
rm -f /etc/nginx/overrides/server.d/mcp-locations.conf
|
|
fi
|
|
|
|
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"
|
|
export PENPOT_INTERNAL_RESOLVER=${PENPOT_INTERNAL_RESOLVER:-$PENPOT_DEFAULT_INTERNAL_RESOLVER}
|
|
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.
|
|
#
|
|
# The hashes of the inline scripts of index.html are emitted by the frontend
|
|
# build and moved to /etc/nginx at image build time. A bundle predating that
|
|
# change simply yields no hashes, in which case those scripts would be
|
|
# reported (or blocked under enforce) as before.
|
|
#
|
|
# It ships in report-only mode because deployments with plugins enabled still
|
|
# report eval and remote fetch violations from the SES sandbox. Enforcing mode
|
|
# stays opt-in until that is resolved.
|
|
export PENPOT_CSP_MODE=${PENPOT_CSP_MODE:-report-only}
|
|
|
|
if [ -r /etc/nginx/csp-script-hashes.txt ]; then
|
|
PENPOT_CSP_SCRIPT_HASHES=" $(tr -d '\n' < /etc/nginx/csp-script-hashes.txt)"
|
|
else
|
|
PENPOT_CSP_SCRIPT_HASHES=""
|
|
fi
|
|
|
|
# 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'${PENPOT_CSP_SCRIPT_HASHES}; 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 degrades the plugin runtime." >&2
|
|
echo "penpot: it initialises on every page load and needs 'unsafe-eval', which the default policy does not grant." >&2
|
|
echo "penpot: set PENPOT_CSP_POLICY to your own policy if you need plugins, or keep report-only." >&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 "$@";
|