feat: add csp headers for embedded apps (#18374)

I modified the proxy host cache we already had and were using for
websocket csp headers to also include the wildcard app host, then used
those for frame-src policies.

I did not add frame-ancestors, since if I understand correctly, those
would go on the app, and this middleware does not come into play there.
Maybe we will want to add it on workspace apps like we do with cors, if
we find apps are setting it to `none` or something.

Closes https://github.com/coder/internal/issues/684
This commit is contained in:
Asher
2025-06-17 09:00:32 -08:00
committed by GitHub
parent aee96c9eac
commit 82c14e00ce
8 changed files with 180 additions and 57 deletions

View File

@ -21,6 +21,8 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/prometheusmetrics"
agplproxyhealth "github.com/coder/coder/v2/coderd/proxyhealth"
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
"github.com/coder/coder/v2/codersdk"
)
@ -63,7 +65,7 @@ type ProxyHealth struct {
// Cached values for quick access to the health of proxies.
cache *atomic.Pointer[map[uuid.UUID]ProxyStatus]
proxyHosts *atomic.Pointer[[]string]
proxyHosts *atomic.Pointer[[]*agplproxyhealth.ProxyHost]
// PromMetrics
healthCheckDuration prometheus.Histogram
@ -116,7 +118,7 @@ func New(opts *Options) (*ProxyHealth, error) {
logger: opts.Logger,
client: client,
cache: &atomic.Pointer[map[uuid.UUID]ProxyStatus]{},
proxyHosts: &atomic.Pointer[[]string]{},
proxyHosts: &atomic.Pointer[[]*agplproxyhealth.ProxyHost]{},
healthCheckDuration: healthCheckDuration,
healthCheckResults: healthCheckResults,
}, nil
@ -144,9 +146,9 @@ func (p *ProxyHealth) Run(ctx context.Context) {
}
func (p *ProxyHealth) storeProxyHealth(statuses map[uuid.UUID]ProxyStatus) {
var proxyHosts []string
var proxyHosts []*agplproxyhealth.ProxyHost
for _, s := range statuses {
if s.ProxyHost != "" {
if s.ProxyHost != nil {
proxyHosts = append(proxyHosts, s.ProxyHost)
}
}
@ -190,23 +192,22 @@ type ProxyStatus struct {
// then the proxy in hand. AKA if the proxy was updated, and the status was for
// an older proxy.
Proxy database.WorkspaceProxy
// ProxyHost is the host:port of the proxy url. This is included in the status
// to make sure the proxy url is a valid URL. It also makes it easier to
// escalate errors if the url.Parse errors (should never happen).
ProxyHost string
// ProxyHost is the base host:port and app host of the proxy. This is included
// in the status to make sure the proxy url is a valid URL. It also makes it
// easier to escalate errors if the url.Parse errors (should never happen).
ProxyHost *agplproxyhealth.ProxyHost
Status Status
Report codersdk.ProxyHealthReport
CheckedAt time.Time
}
// ProxyHosts returns the host:port of all healthy proxies.
// This can be computed from HealthStatus, but is cached to avoid the
// caller needing to loop over all proxies to compute this on all
// static web requests.
func (p *ProxyHealth) ProxyHosts() []string {
// ProxyHosts returns the host:port and wildcard host of all healthy proxies.
// This can be computed from HealthStatus, but is cached to avoid the caller
// needing to loop over all proxies to compute this on all static web requests.
func (p *ProxyHealth) ProxyHosts() []*agplproxyhealth.ProxyHost {
ptr := p.proxyHosts.Load()
if ptr == nil {
return []string{}
return []*agplproxyhealth.ProxyHost{}
}
return *ptr
}
@ -350,7 +351,10 @@ func (p *ProxyHealth) runOnce(ctx context.Context, now time.Time) (map[uuid.UUID
status.Report.Errors = append(status.Report.Errors, fmt.Sprintf("failed to parse proxy url: %s", err.Error()))
status.Status = Unhealthy
}
status.ProxyHost = u.Host
status.ProxyHost = &agplproxyhealth.ProxyHost{
Host: u.Host,
AppHost: appurl.ConvertAppHostForCSP(u.Host, proxy.WildcardHostname),
}
// Set the prometheus metric correctly.
switch status.Status {

View File

@ -965,12 +965,8 @@ func convertRegion(proxy database.WorkspaceProxy, status proxyhealth.ProxyStatus
func convertProxy(p database.WorkspaceProxy, status proxyhealth.ProxyStatus) codersdk.WorkspaceProxy {
now := dbtime.Now()
if p.IsPrimary() {
// Primary is always healthy since the primary serves the api that this
// is returned from.
u, _ := url.Parse(p.Url)
status = proxyhealth.ProxyStatus{
Proxy: p,
ProxyHost: u.Host,
Status: proxyhealth.Healthy,
Report: codersdk.ProxyHealthReport{},
CheckedAt: now,