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
pull/2667/head
Danielle 3 years ago committed by GitHub
parent 4b03a39958
commit 5f8d292ea1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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.

Loading…
Cancel
Save