Inject Custom Response Headers By Status (#2587)
* add custom ResponseWriter struct to inject custom response headers based on status code * docs * upgrade listenerutil * fix config tests * replace all instances of "data:*" with "data:" to reflect changes to listenerutil * swapping strings.HasPrefix in favor of mux.Handler for ui request matching * set ui path as const * add custom response headers to changelog
This commit is contained in:
1 parent
4b03a39958
commit
5f8d292ea1
7 files changed
+322
-36
No files matched your search
@@ -33,6 +33,10 @@ Canonical reference for changes, improvements, and bugfixes for Boundary.
|
||||
|
||||
* `{{ truncateFrom .Account.Email "@" }}`: this would turn `foo@example.com` into `foo`
|
||||
|
||||
* Custom Response Headers: Adds ability to set api and ui response headers based
|
||||
on status code. Includes default secure CSP and other headers.
|
||||
([PR](https://github.com/hashicorp/boundary/pull/2587))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* accounts: Deleted auth accounts would still show up as being associated with a
|
||||
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
github.com/golang-migrate/migrate/v4 v4.15.1
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe
|
||||
github.com/golang/protobuf v1.5.2
|
||||
github.com/google/go-cmp v0.5.8
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.2
|
||||
github.com/hashicorp/boundary/api v0.0.30
|
||||
@@ -38,7 +38,7 @@ require (
|
||||
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.5
|
||||
github.com/hashicorp/go-secure-stdlib/gatedwriter v0.1.1
|
||||
github.com/hashicorp/go-secure-stdlib/kv-builder v0.1.1
|
||||
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.4
|
||||
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.5-0.20221130175209-f7789ac19a1f
|
||||
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7
|
||||
github.com/hashicorp/go-secure-stdlib/password v0.1.1
|
||||
@@ -74,7 +74,7 @@ require (
|
||||
go.uber.org/atomic v1.9.0
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10
|
||||
golang.org/x/sys v0.2.0
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/tools v0.1.12
|
||||
google.golang.org/genproto v0.0.0-20220805133916-01dd62135a58
|
||||
|
||||
@@ -585,8 +585,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v35 v35.2.0/go.mod h1:s0515YVTI+IMrDoy9Y4pHt9ShGpzHvHO8rZ7L7acgvs=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -711,6 +711,10 @@ github.com/hashicorp/go-secure-stdlib/kv-builder v0.1.1 h1:IJgULbAXuvWxzKFfu+Au1
|
||||
github.com/hashicorp/go-secure-stdlib/kv-builder v0.1.1/go.mod h1:rf5JPE13wi+NwjgsmGkbg4b2CgHq8v7Htn/F0nDe/hg=
|
||||
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.4 h1:6ajbq64FhrIJZ6prrff3upVVDil4yfCrnSKwTH0HIPE=
|
||||
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.4/go.mod h1:myX7XYMJRIP4PLHtYJiKMTJcKOX0M5ZJNwP0iw+l3uw=
|
||||
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.5-0.20221122211730-f52ba9532e2c h1:EtRfLZUtPGadI0LGiAHLO3ipQSdJ9J0+/8l8H8v3Znk=
|
||||
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.5-0.20221122211730-f52ba9532e2c/go.mod h1:MSXg3Md+eg1hOJFSKuTELy6YnFhfMJBVYu7t07BdPc4=
|
||||
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.5-0.20221130175209-f7789ac19a1f h1:FemjU7MTEVt5HVI59M1AQjzEaR8mvlXr5I985BUbLSg=
|
||||
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.5-0.20221130175209-f7789ac19a1f/go.mod h1:MSXg3Md+eg1hOJFSKuTELy6YnFhfMJBVYu7t07BdPc4=
|
||||
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc=
|
||||
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
|
||||
@@ -1622,8 +1626,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/cmd/config"
|
||||
@@ -14,6 +15,23 @@ import (
|
||||
)
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
apiHeaders := map[int]http.Header{
|
||||
0: {
|
||||
"Content-Security-Policy": {"default-src 'none'"},
|
||||
"X-Content-Type-Options": {"nosniff"},
|
||||
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
|
||||
"Cache-Control": {"no-store"},
|
||||
},
|
||||
}
|
||||
uiHeaders := map[int]http.Header{
|
||||
0: {
|
||||
"Content-Security-Policy": {"default-src 'none'; script-src 'self'; frame-src 'self'; font-src 'self'; connect-src 'self'; img-src 'self' data:; style-src 'self'; media-src 'self'; manifest-src 'self'; style-src-attr 'self'; frame-ancestors 'self'"},
|
||||
"X-Content-Type-Options": {"nosniff"},
|
||||
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
|
||||
"Cache-Control": {"no-store"},
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
expected *config.Config
|
||||
@@ -91,6 +109,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: []string{"*"},
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
RawConfig: map[string]any{
|
||||
@@ -158,6 +178,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: nil,
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
RawConfig: map[string]any{
|
||||
@@ -225,6 +247,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: nil,
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
RawConfig: map[string]any{
|
||||
@@ -292,6 +316,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: nil,
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
},
|
||||
Seals: []*configutil.KMS{
|
||||
@@ -497,6 +523,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: []string{"*"},
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
RawConfig: map[string]any{
|
||||
@@ -564,6 +592,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: nil,
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
RawConfig: map[string]any{
|
||||
@@ -631,6 +661,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: nil,
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
RawConfig: map[string]any{
|
||||
@@ -698,6 +730,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: nil,
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
},
|
||||
Seals: []*configutil.KMS{
|
||||
@@ -903,6 +937,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: []string{"*"},
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
RawConfig: map[string]any{
|
||||
@@ -970,6 +1006,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: nil,
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
RawConfig: map[string]any{
|
||||
@@ -1037,6 +1075,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: nil,
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
RawConfig: map[string]any{
|
||||
@@ -1104,6 +1144,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: nil,
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
},
|
||||
Seals: []*configutil.KMS{
|
||||
@@ -1309,6 +1351,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: []string{"*"},
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
RawConfig: map[string]any{
|
||||
@@ -1376,6 +1420,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: nil,
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
RawConfig: map[string]any{
|
||||
@@ -1443,6 +1489,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: nil,
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
RawConfig: map[string]any{
|
||||
@@ -1510,6 +1558,8 @@ func TestLoad(t *testing.T) {
|
||||
CorsAllowedOrigins: nil,
|
||||
CorsAllowedHeaders: nil,
|
||||
CorsAllowedHeadersRaw: nil,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
},
|
||||
Seals: []*configutil.KMS{
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -22,26 +23,50 @@ func TestDevController(t *testing.T) {
|
||||
|
||||
truePointer := new(bool)
|
||||
*truePointer = true
|
||||
|
||||
apiHeaders := map[int]http.Header{
|
||||
0: {
|
||||
"Content-Security-Policy": {"default-src 'none'"},
|
||||
"X-Content-Type-Options": {"nosniff"},
|
||||
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
|
||||
"Cache-Control": {"no-store"},
|
||||
},
|
||||
}
|
||||
uiHeaders := map[int]http.Header{
|
||||
0: {
|
||||
"Content-Security-Policy": {"default-src 'none'; script-src 'self'; frame-src 'self'; font-src 'self'; connect-src 'self'; img-src 'self' data:; style-src 'self'; media-src 'self'; manifest-src 'self'; style-src-attr 'self'; frame-ancestors 'self'"},
|
||||
"X-Content-Type-Options": {"nosniff"},
|
||||
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
|
||||
"Cache-Control": {"no-store"},
|
||||
},
|
||||
}
|
||||
|
||||
exp := &Config{
|
||||
Eventing: event.DefaultEventerConfig(),
|
||||
SharedConfig: &configutil.SharedConfig{
|
||||
DisableMlock: true,
|
||||
Listeners: []*listenerutil.ListenerConfig{
|
||||
{
|
||||
Type: "tcp",
|
||||
Purpose: []string{"api"},
|
||||
TLSDisable: true,
|
||||
CorsEnabled: truePointer,
|
||||
CorsAllowedOrigins: []string{"*"},
|
||||
Type: "tcp",
|
||||
Purpose: []string{"api"},
|
||||
TLSDisable: true,
|
||||
CorsEnabled: truePointer,
|
||||
CorsAllowedOrigins: []string{"*"},
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
Type: "tcp",
|
||||
Purpose: []string{"cluster"},
|
||||
Type: "tcp",
|
||||
Purpose: []string{"cluster"},
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
Type: "tcp",
|
||||
Purpose: []string{"ops"},
|
||||
TLSDisable: true,
|
||||
Type: "tcp",
|
||||
Purpose: []string{"ops"},
|
||||
TLSDisable: true,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
},
|
||||
Seals: []*configutil.KMS{
|
||||
@@ -176,6 +201,22 @@ func TestDevWorker(t *testing.T) {
|
||||
{
|
||||
Type: "tcp",
|
||||
Purpose: []string{"proxy"},
|
||||
CustomApiResponseHeaders: map[int]http.Header{
|
||||
0: {
|
||||
"Content-Security-Policy": {"default-src 'none'"},
|
||||
"X-Content-Type-Options": {"nosniff"},
|
||||
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
|
||||
"Cache-Control": {"no-store"},
|
||||
},
|
||||
},
|
||||
CustomUiResponseHeaders: map[int]http.Header{
|
||||
0: {
|
||||
"Content-Security-Policy": {"default-src 'none'; script-src 'self'; frame-src 'self'; font-src 'self'; connect-src 'self'; img-src 'self' data:; style-src 'self'; media-src 'self'; manifest-src 'self'; style-src-attr 'self'; frame-ancestors 'self'"},
|
||||
"X-Content-Type-Options": {"nosniff"},
|
||||
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
|
||||
"Cache-Control": {"no-store"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Seals: []*configutil.KMS{
|
||||
@@ -329,30 +370,56 @@ func TestDevCombined(t *testing.T) {
|
||||
|
||||
truePointer := new(bool)
|
||||
*truePointer = true
|
||||
|
||||
apiHeaders := map[int]http.Header{
|
||||
0: {
|
||||
"Content-Security-Policy": {"default-src 'none'"},
|
||||
"X-Content-Type-Options": {"nosniff"},
|
||||
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
|
||||
"Cache-Control": {"no-store"},
|
||||
},
|
||||
}
|
||||
uiHeaders := map[int]http.Header{
|
||||
0: {
|
||||
"Content-Security-Policy": {"default-src 'none'; script-src 'self'; frame-src 'self'; font-src 'self'; connect-src 'self'; img-src 'self' data:; style-src 'self'; media-src 'self'; manifest-src 'self'; style-src-attr 'self'; frame-ancestors 'self'"},
|
||||
"X-Content-Type-Options": {"nosniff"},
|
||||
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
|
||||
"Cache-Control": {"no-store"},
|
||||
},
|
||||
}
|
||||
|
||||
exp := &Config{
|
||||
Eventing: event.DefaultEventerConfig(),
|
||||
SharedConfig: &configutil.SharedConfig{
|
||||
DisableMlock: true,
|
||||
Listeners: []*listenerutil.ListenerConfig{
|
||||
{
|
||||
Type: "tcp",
|
||||
Purpose: []string{"api"},
|
||||
TLSDisable: true,
|
||||
CorsEnabled: truePointer,
|
||||
CorsAllowedOrigins: []string{"*"},
|
||||
Type: "tcp",
|
||||
Purpose: []string{"api"},
|
||||
TLSDisable: true,
|
||||
CorsEnabled: truePointer,
|
||||
CorsAllowedOrigins: []string{"*"},
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
Type: "tcp",
|
||||
Purpose: []string{"cluster"},
|
||||
Type: "tcp",
|
||||
Purpose: []string{"cluster"},
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
Type: "tcp",
|
||||
Purpose: []string{"ops"},
|
||||
TLSDisable: true,
|
||||
Type: "tcp",
|
||||
Purpose: []string{"ops"},
|
||||
TLSDisable: true,
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
{
|
||||
Type: "tcp",
|
||||
Purpose: []string{"proxy"},
|
||||
Type: "tcp",
|
||||
Purpose: []string{"proxy"},
|
||||
CustomApiResponseHeaders: apiHeaders,
|
||||
CustomUiResponseHeaders: uiHeaders,
|
||||
},
|
||||
},
|
||||
Seals: []*configutil.KMS{
|
||||
|
||||
@@ -57,18 +57,36 @@ type HandlerProperties struct {
|
||||
CancelCtx context.Context
|
||||
}
|
||||
|
||||
const uiPath = "/"
|
||||
|
||||
// createMuxWithEndpoints performs all response logic for boundary, using isUiRequest
|
||||
// for unified logic between responses and headers.
|
||||
func createMuxWithEndpoints(c *Controller, props HandlerProperties) (http.Handler, func(req *http.Request) bool, error) {
|
||||
grpcGwMux := newGrpcGatewayMux()
|
||||
if err := registerGrpcGatewayEndpoints(props.CancelCtx, grpcGwMux, gatewayDialOptions(c.apiGrpcServerListener)...); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/v1/", grpcGwMux)
|
||||
mux.Handle(uiPath, handleUi(c))
|
||||
|
||||
isUiRequest := func(req *http.Request) bool {
|
||||
_, p := mux.Handler(req)
|
||||
// check to see if the matched pattern is for the ui
|
||||
return p == uiPath
|
||||
}
|
||||
|
||||
return http.HandlerFunc(mux.ServeHTTP), isUiRequest, nil
|
||||
}
|
||||
|
||||
// apiHandler returns an http.Handler for the services. This can be used on
|
||||
// its own to mount the Controller API within another web server.
|
||||
func (c *Controller) apiHandler(props HandlerProperties) (http.Handler, error) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
grpcGwMux := newGrpcGatewayMux()
|
||||
err := registerGrpcGatewayEndpoints(props.CancelCtx, grpcGwMux, gatewayDialOptions(c.apiGrpcServerListener)...)
|
||||
mux, isUiRequest, err := createMuxWithEndpoints(c, props)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mux.Handle("/v1/", grpcGwMux)
|
||||
mux.Handle("/", handleUi(c))
|
||||
|
||||
corsWrappedHandler := wrapHandlerWithCors(mux, props)
|
||||
commonWrappedHandler := wrapHandlerWithCommonFuncs(corsWrappedHandler, c, props)
|
||||
@@ -80,7 +98,8 @@ func (c *Controller) apiHandler(props HandlerProperties) (http.Handler, error) {
|
||||
}
|
||||
metricsHandler := metric.InstrumentApiHandler(eventsHandler)
|
||||
|
||||
return metricsHandler, nil
|
||||
// This wrap MUST be performed last. If you add a new wrapper, do so above.
|
||||
return listenerutil.WrapCustomHeadersHandler(metricsHandler, props.ListenerConfig, isUiRequest), nil
|
||||
}
|
||||
|
||||
// GetHealthHandler returns a gRPC Gateway mux that is registered against the
|
||||
|
||||
@@ -21,6 +21,22 @@ The `listener` stanza may be specified more than once to make Boundary listen on
|
||||
multiple interfaces; however, only one listener marked for `cluster` purpose is
|
||||
allowed.
|
||||
|
||||
## Listener's custom response headers
|
||||
|
||||
Boundary supports defining custom HTTP response headers for all requests on any Boundary controller.
|
||||
Headers are defined based on the returned status code. For example, you can define a list of
|
||||
custom response headers for the `200` status code, and another list of custom response headers for
|
||||
the `307` status code, and so on. You can also define headers based on the hundred-level status
|
||||
code. For example, a list of headers applied to the `4xx` code will be applied to 400, 401, 404, and
|
||||
all other 400-level status codes. The more specific the status, the higher priority it has.
|
||||
Default headers are overwritten by hundred-level headers, which are overwritten by status-specific
|
||||
headers.
|
||||
|
||||
There are two different config parameters that define headers: `custom_api_response_headers` and
|
||||
`custom_ui_response_headers`. API headers apply to API endpoints, currently all paths starting
|
||||
with `/v1/`. UI headers apply to all other paths. This allows for configuring headers specifically
|
||||
for serving content to a web browser, such as CSP headers.
|
||||
|
||||
## `tcp` Listener Parameters
|
||||
|
||||
### General
|
||||
@@ -84,6 +100,48 @@ allowed.
|
||||
default, such as `"Content-Type"`, `"X-Requested-With"`, and
|
||||
`"Authorization"`.
|
||||
|
||||
### `custom_api_response_headers` Parameters
|
||||
|
||||
- `default` `(key-value-map: {})` - A map of string header names to an array of
|
||||
string values. The default headers are set on all endpoints regardless of
|
||||
the status code value. For an example, refer to the
|
||||
[Configuring custom http response headers](#configuring-custom-http-response-headers)
|
||||
section.
|
||||
|
||||
- `<collective status code>` `(key-value-map: {})` - A map of string header names
|
||||
to an array of string values. These headers are set only when the response status
|
||||
code falls under the collective status code.
|
||||
For example, `"2xx" = {"Header-A": ["Value1", "Value2"]}`, `"Header-A"`
|
||||
is set when the http response status code is `"200"`, `"204"`, etc. This overrides
|
||||
headers defined at the `default` level.
|
||||
|
||||
- `<specific status code>` `(key-value-map: {})` - A map of string header names
|
||||
to an array of string values. These headers are set only when the specific status
|
||||
code is returned. For example, `"200" = {"Header-A": ["Value1", "Value2"]}`, `"Header-A"`
|
||||
is set when the http response status code is `"200"`. This overrides headers defined at
|
||||
the `default` and `collective status code` level.
|
||||
|
||||
### `custom_ui_response_headers` Parameters
|
||||
|
||||
- `default` `(key-value-map: {})` - A map of string header names to an array of
|
||||
string values. The default headers are set on all endpoints regardless of
|
||||
the status code value. For an example, refer to the
|
||||
[Configuring custom http response headers](#configuring-custom-http-response-headers)
|
||||
section.
|
||||
|
||||
- `<collective status code>` `(key-value-map: {})` - A map of string header names
|
||||
to an array of string values. These headers are set only when the response status
|
||||
code falls under the collective status code.
|
||||
For example, `"2xx" = {"Header-A": ["Value1", "Value2"]}`, `"Header-A"`
|
||||
is set when the http response status code is `"200"`, `"204"`, etc. This overrides
|
||||
headers defined at the `default` level.
|
||||
|
||||
- `<specific status code>` `(key-value-map: {})` - A map of string header names
|
||||
to an array of string values. These headers are set only when the specific status
|
||||
code is returned. For example, `"200" = {"Header-A": ["Value1", "Value2"]}`, `"Header-A"`
|
||||
is set when the http response status code is `"200"`. This overrides headers defined at
|
||||
the `default` and `collective status code` level.
|
||||
|
||||
### TLS
|
||||
|
||||
~> `tls` parameters are valid for `api` and `ops` listeners. `cluster`
|
||||
@@ -177,6 +235,90 @@ listener "tcp" {
|
||||
}
|
||||
```
|
||||
|
||||
### Configuring custom http response headers
|
||||
|
||||
This example shows configuring custom http response headers. Operators can configure
|
||||
`"custom_api_response_headers"` and `"custom_ui_response_headers"` sub-stanzas in the listener stanza to
|
||||
set custom http headers that are appropriate to their applications. Examples of such headers are
|
||||
`"Strict-Transport-Security"` and `"Content-Security-Policy"` which are known HTTP headers, and could be
|
||||
configured to harden the security of an application communicating with the Boundary endpoints. Note that
|
||||
vulnerability scans often examine such security related HTTP headers. In addition, you can configure
|
||||
application-specific custom headers. For example, `"X-Custom-Header"` has been configured in the example
|
||||
below.
|
||||
|
||||
```hcl
|
||||
listener "tcp" {
|
||||
custom_api_response_headers {
|
||||
"default" = {
|
||||
"Strict-Transport-Security" = ["max-age=31536000; includeSubDomains"],
|
||||
"X-Custom-Header" = ["Custom Header Default Value"],
|
||||
},
|
||||
"2xx" = {
|
||||
"X-Custom-Header" = ["Custom Header Value 1", "Custom Header Value 2"],
|
||||
},
|
||||
"301" = {
|
||||
"Strict-Transport-Security" = ["max-age=31536000"],
|
||||
},
|
||||
}
|
||||
custom_ui_response_headers {
|
||||
"default" = {
|
||||
"Strict-Transport-Security" = ["max-age=31536000; includeSubDomains"],
|
||||
"Content-Security-Policy" = ["default-src 'none'; script-src 'self'; frame-src 'self'; font-src 'self'; connect-src 'self'; img-src 'self' data:; style-src 'self'; media-src 'self'; manifest-src 'self'; style-src-attr 'self'; frame-ancestors 'self'"],
|
||||
"X-Custom-Header" = ["Custom Header Default Value"],
|
||||
},
|
||||
"2xx" = {
|
||||
"X-Custom-Header" = ["Custom Header Value 1", "Custom Header Value 2"],
|
||||
},
|
||||
"301" = {
|
||||
"Strict-Transport-Security" = ["max-age=31536000"],
|
||||
"Content-Security-Policy" = ["default-src 'none'; script-src 'none'; connect-src 'none'"],
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In situations where a header is defined under several status code subsections,
|
||||
Boundary returns the header matching the most specific response code. For example,
|
||||
with the config example below, a `307` response would return `307 Custom header value`,
|
||||
while a `306` would return `3xx Custom header value`.
|
||||
|
||||
```hcl
|
||||
listener "tcp" {
|
||||
custom_api_response_headers {
|
||||
"default" = {
|
||||
"X-Custom-Header" = ["default Custom header value"]
|
||||
},
|
||||
"3xx" = {
|
||||
"X-Custom-Header" = ["3xx Custom header value"]
|
||||
},
|
||||
"307" = {
|
||||
"X-Custom-Header" = ["307 Custom header value"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
There may also be situations where default or collective status headers have been defined,
|
||||
but you do not want them returned for a specific status code. You can unset headers at any
|
||||
level by defining the headers as an empty list.
|
||||
|
||||
```hcl
|
||||
listener "tcp" {
|
||||
custom_api_response_headers {
|
||||
"default" = {
|
||||
"X-Custom-Header" = ["default Custom header value"]
|
||||
},
|
||||
"3xx" = {
|
||||
"X-Custom-Header" = ["3xx Custom header value"]
|
||||
},
|
||||
"307" = {
|
||||
// Do not return this header for 307 responses
|
||||
"X-Custom-Header" = []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Listening on Multiple Interfaces
|
||||
|
||||
This example shows Boundary listening on a private interface, as well as localhost.
|
||||
|
||||
Reference in new issue
Block a user