Skip to content

Commit e0bfb73

Browse files
authored
Customizing HTTP headers in the config file (#12485)
* Customizing HTTP headers in the config file * Add changelog, fix bad imports * fixing some bugs * fixing interaction of custom headers and /ui * Defining a member in core to set custom response headers * missing additional file * Some refactoring * Adding automated tests for the feature * Changing some error messages based on some recommendations * Incorporating custom response headers struct into the request context * removing some unused references * fixing a test * changing some error messages, removing a default header value from /ui * fixing a test * wrapping ResponseWriter to set the custom headers * adding a new test * some cleanup * removing some extra lines * Addressing comments * fixing some agent tests * skipping custom headers from agent listener config, removing two of the default headers as they cause issues with Vault in UI mode Adding X-Content-Type-Options to the ui default headers Let Content-Type be set as before * Removing default custom headers, and renaming some function varibles * some refacotring * Refactoring and addressing comments * removing a function and fixing comments
1 parent 3aafbd0 commit e0bfb73

21 files changed

+1019
-23
lines changed

changelog/12485.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:feature
2+
**Customizable HTTP Headers**: Add support to define custom HTTP headers for root path (`/`) and also on API endpoints (`/v1/*`)
3+
```

command/agent/config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func (c *Config) Prune() {
3535
l.RawConfig = nil
3636
l.Profiling.UnusedKeys = nil
3737
l.Telemetry.UnusedKeys = nil
38+
l.CustomResponseHeaders = nil
3839
}
3940
c.FoundKeys = nil
4041
c.UnusedKeys = nil
@@ -172,6 +173,12 @@ func LoadConfig(path string) (*Config, error) {
172173
if err != nil {
173174
return nil, err
174175
}
176+
177+
// Pruning custom headers for Agent for now
178+
for _, ln := range sharedConfig.Listeners {
179+
ln.CustomResponseHeaders = nil
180+
}
181+
175182
result.SharedConfig = sharedConfig
176183

177184
list, ok := obj.Node.(*ast.ObjectList)

command/agent/config/config_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,6 @@ func TestLoadConfigFile_AgentCache_PersistMissingType(t *testing.T) {
536536
}
537537

538538
func TestLoadConfigFile_TemplateConfig(t *testing.T) {
539-
540539
testCases := map[string]struct {
541540
fixturePath string
542541
expectedTemplateConfig TemplateConfig
@@ -586,7 +585,6 @@ func TestLoadConfigFile_TemplateConfig(t *testing.T) {
586585
}
587586
})
588587
}
589-
590588
}
591589

592590
// TestLoadConfigFile_Template tests template definitions in Vault Agent

command/server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,6 +1541,12 @@ func (c *ServerCommand) Run(args []string) int {
15411541

15421542
core.SetConfig(config)
15431543

1544+
// reloading custom response headers to make sure we have
1545+
// the most up to date headers after reloading the config file
1546+
if err = core.ReloadCustomResponseHeaders(); err != nil {
1547+
c.logger.Error(err.Error())
1548+
}
1549+
15441550
if config.LogLevel != "" {
15451551
configLogLevel := strings.ToLower(strings.TrimSpace(config.LogLevel))
15461552
switch configLogLevel {
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package server
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/go-test/deep"
8+
)
9+
10+
var defaultCustomHeaders = map[string]string{
11+
"Strict-Transport-Security": "max-age=1; domains",
12+
"Content-Security-Policy": "default-src 'others'",
13+
"X-Vault-Ignored": "ignored",
14+
"X-Custom-Header": "Custom header value default",
15+
}
16+
17+
var customHeaders307 = map[string]string{
18+
"X-Custom-Header": "Custom header value 307",
19+
}
20+
21+
var customHeader3xx = map[string]string{
22+
"X-Vault-Ignored-3xx": "Ignored 3xx",
23+
"X-Custom-Header": "Custom header value 3xx",
24+
}
25+
26+
var customHeaders200 = map[string]string{
27+
"Someheader-200": "200",
28+
"X-Custom-Header": "Custom header value 200",
29+
}
30+
31+
var customHeader2xx = map[string]string{
32+
"X-Custom-Header": "Custom header value 2xx",
33+
}
34+
35+
var customHeader400 = map[string]string{
36+
"Someheader-400": "400",
37+
}
38+
39+
var defaultCustomHeadersMultiListener = map[string]string{
40+
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
41+
"Content-Security-Policy": "default-src 'others'",
42+
"X-Vault-Ignored": "ignored",
43+
"X-Custom-Header": "Custom header value default",
44+
}
45+
46+
var defaultSTS = map[string]string{
47+
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
48+
}
49+
50+
func TestCustomResponseHeadersConfigs(t *testing.T) {
51+
expectedCustomResponseHeader := map[string]map[string]string{
52+
"default": defaultCustomHeaders,
53+
"307": customHeaders307,
54+
"3xx": customHeader3xx,
55+
"200": customHeaders200,
56+
"2xx": customHeader2xx,
57+
"400": customHeader400,
58+
}
59+
60+
config, err := LoadConfigFile("./test-fixtures/config_custom_response_headers_1.hcl")
61+
if err != nil {
62+
t.Fatalf("Error encountered when loading config %+v", err)
63+
}
64+
if diff := deep.Equal(expectedCustomResponseHeader, config.Listeners[0].CustomResponseHeaders); diff != nil {
65+
t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff))
66+
}
67+
}
68+
69+
func TestCustomResponseHeadersConfigsMultipleListeners(t *testing.T) {
70+
expectedCustomResponseHeader := map[string]map[string]string{
71+
"default": defaultCustomHeadersMultiListener,
72+
"307": customHeaders307,
73+
"3xx": customHeader3xx,
74+
"200": customHeaders200,
75+
"2xx": customHeader2xx,
76+
"400": customHeader400,
77+
}
78+
79+
config, err := LoadConfigFile("./test-fixtures/config_custom_response_headers_multiple_listeners.hcl")
80+
if err != nil {
81+
t.Fatalf("Error encountered when loading config %+v", err)
82+
}
83+
if diff := deep.Equal(expectedCustomResponseHeader, config.Listeners[0].CustomResponseHeaders); diff != nil {
84+
t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff))
85+
}
86+
87+
if diff := deep.Equal(expectedCustomResponseHeader, config.Listeners[1].CustomResponseHeaders); diff == nil {
88+
t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff))
89+
}
90+
if diff := deep.Equal(expectedCustomResponseHeader["default"], config.Listeners[1].CustomResponseHeaders["default"]); diff != nil {
91+
t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff))
92+
}
93+
94+
if diff := deep.Equal(expectedCustomResponseHeader, config.Listeners[2].CustomResponseHeaders); diff == nil {
95+
t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff))
96+
}
97+
98+
if diff := deep.Equal(defaultSTS, config.Listeners[2].CustomResponseHeaders["default"]); diff != nil {
99+
t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff))
100+
}
101+
102+
if diff := deep.Equal(expectedCustomResponseHeader, config.Listeners[3].CustomResponseHeaders); diff == nil {
103+
t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff))
104+
}
105+
106+
if diff := deep.Equal(defaultSTS, config.Listeners[3].CustomResponseHeaders["default"]); diff != nil {
107+
t.Fatalf(fmt.Sprintf("parsed custom headers do not match the expected ones, difference: %v", diff))
108+
}
109+
}

command/server/config_test_helpers.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import (
1616
"github.com/hashicorp/vault/internalshared/configutil"
1717
)
1818

19+
var DefaultCustomHeaders = map[string]map[string]string {
20+
"default": {
21+
"Strict-Transport-Security": configutil.StrictTransportSecurity,
22+
},
23+
}
24+
1925
func boolPointer(x bool) *bool {
2026
return &x
2127
}
@@ -32,6 +38,7 @@ func testConfigRaftRetryJoin(t *testing.T) {
3238
{
3339
Type: "tcp",
3440
Address: "127.0.0.1:8200",
41+
CustomResponseHeaders: DefaultCustomHeaders,
3542
},
3643
},
3744
DisableMlock: true,
@@ -64,6 +71,7 @@ func testLoadConfigFile_topLevel(t *testing.T, entropy *configutil.Entropy) {
6471
{
6572
Type: "tcp",
6673
Address: "127.0.0.1:443",
74+
CustomResponseHeaders: DefaultCustomHeaders,
6775
},
6876
},
6977

@@ -174,10 +182,12 @@ func testLoadConfigFile_json2(t *testing.T, entropy *configutil.Entropy) {
174182
{
175183
Type: "tcp",
176184
Address: "127.0.0.1:443",
185+
CustomResponseHeaders: DefaultCustomHeaders,
177186
},
178187
{
179188
Type: "tcp",
180189
Address: "127.0.0.1:444",
190+
CustomResponseHeaders: DefaultCustomHeaders,
181191
},
182192
},
183193

@@ -336,6 +346,7 @@ func testLoadConfigFileIntegerAndBooleanValuesCommon(t *testing.T, path string)
336346
{
337347
Type: "tcp",
338348
Address: "127.0.0.1:8200",
349+
CustomResponseHeaders: DefaultCustomHeaders,
339350
},
340351
},
341352
DisableMlock: true,
@@ -379,6 +390,7 @@ func testLoadConfigFile(t *testing.T) {
379390
{
380391
Type: "tcp",
381392
Address: "127.0.0.1:443",
393+
CustomResponseHeaders: DefaultCustomHeaders,
382394
},
383395
},
384396

@@ -486,7 +498,7 @@ func testUnknownFieldValidation(t *testing.T) {
486498
for _, er1 := range errors {
487499
found := false
488500
if strings.Contains(er1.String(), "sentinel") {
489-
//This happens on OSS, and is fine
501+
// This happens on OSS, and is fine
490502
continue
491503
}
492504
for _, ex := range expected {
@@ -525,6 +537,7 @@ func testLoadConfigFile_json(t *testing.T) {
525537
{
526538
Type: "tcp",
527539
Address: "127.0.0.1:443",
540+
CustomResponseHeaders: DefaultCustomHeaders,
528541
},
529542
},
530543

@@ -610,6 +623,7 @@ func testLoadConfigDir(t *testing.T) {
610623
{
611624
Type: "tcp",
612625
Address: "127.0.0.1:443",
626+
CustomResponseHeaders: DefaultCustomHeaders,
613627
},
614628
},
615629

@@ -818,6 +832,7 @@ listener "tcp" {
818832
Profiling: configutil.ListenerProfiling{
819833
UnauthenticatedPProfAccess: true,
820834
},
835+
CustomResponseHeaders: DefaultCustomHeaders,
821836
},
822837
},
823838
},
@@ -845,6 +860,7 @@ func testParseSeals(t *testing.T) {
845860
{
846861
Type: "tcp",
847862
Address: "127.0.0.1:443",
863+
CustomResponseHeaders: DefaultCustomHeaders,
848864
},
849865
},
850866
Seals: []*configutil.KMS{
@@ -898,6 +914,7 @@ func testLoadConfigFileLeaseMetrics(t *testing.T) {
898914
{
899915
Type: "tcp",
900916
Address: "127.0.0.1:443",
917+
CustomResponseHeaders: DefaultCustomHeaders,
901918
},
902919
},
903920

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
storage "inmem" {}
2+
listener "tcp" {
3+
address = "127.0.0.1:8200"
4+
tls_disable = true
5+
custom_response_headers {
6+
"default" = {
7+
"Strict-Transport-Security" = ["max-age=1","domains"],
8+
"Content-Security-Policy" = ["default-src 'others'"],
9+
"X-Vault-Ignored" = ["ignored"],
10+
"X-Custom-Header" = ["Custom header value default"],
11+
}
12+
"307" = {
13+
"X-Custom-Header" = ["Custom header value 307"],
14+
}
15+
"3xx" = {
16+
"X-Vault-Ignored-3xx" = ["Ignored 3xx"],
17+
"X-Custom-Header" = ["Custom header value 3xx"]
18+
}
19+
"200" = {
20+
"someheader-200" = ["200"],
21+
"X-Custom-Header" = ["Custom header value 200"]
22+
}
23+
"2xx" = {
24+
"X-Custom-Header" = ["Custom header value 2xx"]
25+
}
26+
"400" = {
27+
"someheader-400" = ["400"]
28+
}
29+
}
30+
}
31+
disable_mlock = true
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
storage "inmem" {}
2+
listener "tcp" {
3+
address = "127.0.0.1:8200"
4+
tls_disable = true
5+
custom_response_headers {
6+
"default" = {
7+
"Content-Security-Policy" = ["default-src 'others'"],
8+
"X-Vault-Ignored" = ["ignored"],
9+
"X-Custom-Header" = ["Custom header value default"],
10+
}
11+
"307" = {
12+
"X-Custom-Header" = ["Custom header value 307"],
13+
}
14+
"3xx" = {
15+
"X-Vault-Ignored-3xx" = ["Ignored 3xx"],
16+
"X-Custom-Header" = ["Custom header value 3xx"]
17+
}
18+
"200" = {
19+
"someheader-200" = ["200"],
20+
"X-Custom-Header" = ["Custom header value 200"]
21+
}
22+
"2xx" = {
23+
"X-Custom-Header" = ["Custom header value 2xx"]
24+
}
25+
"400" = {
26+
"someheader-400" = ["400"]
27+
}
28+
}
29+
}
30+
listener "tcp" {
31+
address = "127.0.0.2:8200"
32+
tls_disable = true
33+
custom_response_headers {
34+
"default" = {
35+
"Content-Security-Policy" = ["default-src 'others'"],
36+
"X-Vault-Ignored" = ["ignored"],
37+
"X-Custom-Header" = ["Custom header value default"],
38+
}
39+
}
40+
}
41+
listener "tcp" {
42+
address = "127.0.0.3:8200"
43+
tls_disable = true
44+
custom_response_headers {
45+
"2xx" = {
46+
"X-Custom-Header" = ["Custom header value 2xx"]
47+
}
48+
}
49+
}
50+
listener "tcp" {
51+
address = "127.0.0.4:8200"
52+
tls_disable = true
53+
}
54+
55+
56+
disable_mlock = true

0 commit comments

Comments
 (0)