Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for enabling HSTS for TLS requests #424

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ type Proxy struct {
TLSHeaderValue string
GZIPContentTypes *regexp.Regexp
RequestID string
STSHeader STSHeader
}

type STSHeader struct {
MaxAge int
Subdomains bool
Preload bool
}

type Runtime struct {
Expand Down
3 changes: 3 additions & 0 deletions config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ func load(cmdline, environ, envprefix []string, props *properties.Properties) (c
f.StringVar(&cfg.Proxy.TLSHeader, "proxy.header.tls", defaultConfig.Proxy.TLSHeader, "header for TLS connections")
f.StringVar(&cfg.Proxy.TLSHeaderValue, "proxy.header.tls.value", defaultConfig.Proxy.TLSHeaderValue, "value for TLS connection header")
f.StringVar(&cfg.Proxy.RequestID, "proxy.header.requestid", defaultConfig.Proxy.RequestID, "header for reqest id")
f.IntVar(&cfg.Proxy.STSHeader.MaxAge, "proxy.header.sts.maxage", defaultConfig.Proxy.STSHeader.MaxAge, "enable and set the max-age value for HSTS")
f.BoolVar(&cfg.Proxy.STSHeader.Subdomains, "proxy.header.sts.subdomains", defaultConfig.Proxy.STSHeader.Subdomains, "direct HSTS to include subdomains")
f.BoolVar(&cfg.Proxy.STSHeader.Preload, "proxy.header.sts.preload", defaultConfig.Proxy.STSHeader.Preload, "direct HSTS to pass the preload directive")
f.StringVar(&gzipContentTypesValue, "proxy.gzip.contenttype", defaultValues.GZIPContentTypesValue, "regexp of content types to compress")
f.StringVar(&listenerValue, "proxy.addr", defaultValues.ListenerValue, "listener config")
f.StringVar(&certSourcesValue, "proxy.cs", defaultValues.CertSourcesValue, "certificate sources")
Expand Down
32 changes: 32 additions & 0 deletions fabio.properties
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,38 @@
# proxy.header.requestid =


# proxy.header.sts.maxage enables and configures the max-age of HSTS for TLS requests.
# When set greater than zero this enables the Strict-Transport-Security header
# and sets the max-age value in the header.
#
# The default is
#
# proxy.header.sts.maxage = 0


# proxy.header.sts.subdomains instructs HSTS to include subdomains.
# When set to true, the 'includeSubDomains' option will be added to
# the Strict-Transport-Security header.
#
# The default is
#
# proxy.header.sts.subdomains = false


# proxy.header.sts.preload instructs HSTS to include the preload directive.
# When set to true, the 'preload' option will be added to the
# Strict-Transport-Security header.
#
# Sending the preload directive from your site can have PERMANENT CONSEQUENCES
# and prevent users from accessing your site and any of its subdomains if you
# find you need to switch back to HTTP. Please read the details at
# hstspreload.appspot.com/#removal before sending the header with "preload".
#
# The default is
#
# proxy.header.sts.preload = false


# proxy.gzip.contenttype configures which responses should be compressed.
#
# By default, responses sent to the client are not compressed even if the
Expand Down
34 changes: 34 additions & 0 deletions proxy/http_headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ func addHeaders(r *http.Request, cfg config.Proxy, stripPath string) error {
}
}

if r.TLS != nil && cfg.STSHeader.MaxAge > 0 {
sts := "max-age=" + i32toa(int32(cfg.STSHeader.MaxAge))
if cfg.STSHeader.Subdomains {
sts += "; includeSubdomains"
}
if cfg.STSHeader.Preload {
sts += "; preload"
}
r.Header.Set("Strict-Transport-Security", sts)
}

return nil
}

Expand All @@ -130,6 +141,29 @@ func uint16base16(n uint16) string {
return string(b)
}

// i32toa is a faster implentation of strconv.Itoa() without importing another library
// https://stackoverflow.com/a/39444005
func i32toa(n int32) string {
buf := [11]byte{}
pos := len(buf)
i := int64(n)
signed := i < 0
if signed {
i = -i
}
for {
pos--
buf[pos], i = '0'+byte(i%10), i/10
if i == 0 {
if signed {
pos--
buf[pos] = '-'
}
return string(buf[pos:])
}
}
}

// scheme derives the request scheme used on the initial
// request first from headers and then from the connection
// using the following heuristic:
Expand Down
41 changes: 41 additions & 0 deletions proxy/http_headers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,47 @@ func TestAddHeaders(t *testing.T) {
},
"",
},

{"skip Strict-Transport-Security for non-TLS, if MaxAge greater than 0",
&http.Request{RemoteAddr: "1.2.3.4:5555"},
config.Proxy{STSHeader: config.STSHeader{MaxAge: 31536000}},
"",
http.Header{
"Forwarded": []string{"for=1.2.3.4; proto=http"},
"X-Forwarded-Proto": []string{"http"},
"X-Forwarded-Port": []string{"80"},
"X-Real-Ip": []string{"1.2.3.4"},
},
"",
},

{"set Strict-Transport-Security for TLS, if MaxAge greater than 0",
&http.Request{RemoteAddr: "1.2.3.4:5555", TLS: &tls.ConnectionState{}},
config.Proxy{STSHeader: config.STSHeader{MaxAge: 31536000}},
"",
http.Header{
"Forwarded": []string{"for=1.2.3.4; proto=https"},
"Strict-Transport-Security": []string{"max-age=31536000"},
"X-Forwarded-Proto": []string{"https"},
"X-Forwarded-Port": []string{"443"},
"X-Real-Ip": []string{"1.2.3.4"},
},
"",
},

{"set Strict-Transport-Security with options for TLS, if MaxAge greater than 0 with options",
&http.Request{RemoteAddr: "1.2.3.4:5555", TLS: &tls.ConnectionState{}},
config.Proxy{STSHeader: config.STSHeader{MaxAge: 31536000, Preload: true, Subdomains: true}},
"",
http.Header{
"Forwarded": []string{"for=1.2.3.4; proto=https"},
"Strict-Transport-Security": []string{"max-age=31536000; includeSubdomains; preload"},
"X-Forwarded-Proto": []string{"https"},
"X-Forwarded-Port": []string{"443"},
"X-Real-Ip": []string{"1.2.3.4"},
},
"",
},
}

for i, tt := range tests {
Expand Down