Skip to content

feat: Add strict transport security and secure cookie options #741

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

Merged
merged 6 commits into from
Mar 31, 2022

Conversation

f0ssel
Copy link
Contributor

@f0ssel f0ssel commented Mar 30, 2022

No description provided.

@f0ssel f0ssel requested review from kylecarbs and Emyrk March 30, 2022 19:47
@codecov
Copy link

codecov bot commented Mar 30, 2022

Codecov Report

Merging #741 (49f2e75) into main (6ab1a68) will decrease coverage by 0.37%.
The diff coverage is 100.00%.

@@            Coverage Diff             @@
##             main     #741      +/-   ##
==========================================
- Coverage   64.31%   63.93%   -0.38%     
==========================================
  Files         198      199       +1     
  Lines       11635    11830     +195     
  Branches       87       87              
==========================================
+ Hits         7483     7564      +81     
- Misses       3351     3441      +90     
- Partials      801      825      +24     
Flag Coverage Δ
unittest-go- 63.03% <100.00%> (-0.61%) ⬇️
unittest-go-macos-latest 58.98% <100.00%> (-0.05%) ⬇️
unittest-go-ubuntu-latest 61.79% <100.00%> (-0.21%) ⬇️
unittest-go-windows-2022 58.07% <100.00%> (-0.23%) ⬇️
unittest-js 62.63% <ø> (ø)
Impacted Files Coverage Δ
coderd/coderd.go 96.47% <ø> (ø)
cli/start.go 66.00% <100.00%> (-0.16%) ⬇️
coderd/users.go 57.41% <100.00%> (+0.05%) ⬆️
coderd/database/db.go 55.17% <0.00%> (-13.80%) ⬇️
coderd/provisionerdaemons.go 59.52% <0.00%> (-4.56%) ⬇️
coderd/parameter/compute.go 74.07% <0.00%> (-4.45%) ⬇️
cli/ssh.go 38.88% <0.00%> (-4.45%) ⬇️
agent/conn.go 65.38% <0.00%> (-3.59%) ⬇️
agent/agent.go 67.30% <0.00%> (-1.09%) ⬇️
peer/conn.go 75.38% <0.00%> (-1.02%) ⬇️
... and 15 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6ab1a68...49f2e75. Read the comment docs.

Emyrk
Emyrk previously requested changes Mar 30, 2022
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if hsts {
w.Header().Set(HSTSHeader, fmt.Sprintf("max-age=%d", int64(HSTSMaxAge)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The time should be in seconds. Golang times are int64 in nanoseconds

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make the value of this header configurable by the flag? With "true" defaulting to our max-age? Seems like there's a swath of options that we could set, but might not have the best idea when it's appropriate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idk, HSTS should be applied to a long time. Like 1-2 years. Anything shorter is strange. Idk why you'd set it short.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just stick with on/off

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an option includeSubdomains you can add too. What happens when a customer asks for it? Unless we're confident they won't, it's really simple for us to add now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think adding things that aren't required is a big part of what helped get v1 where it is today. No one asking for this is a bet I would take 10/10 times right now.

I could see an argument for changing this to CODER_STRICT_SECURITY_HEADER with the default value "" in which we don't set it, and otherwise they provide the value for the header - like "max-age=63072000;"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your argument is fair. It's weird to offer something nobody is asking for, and this is easy to change later too!

t.Parallel()

res := setup(true)
require.Contains(t, res.Header.Get(httpmw.HSTSHeader), fmt.Sprintf("max-age=%d", int64(httpmw.HSTSMaxAge)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather us hard code the seconds integer, given that this value is likely massively too high atm.

t.Parallel()

res := setup(false)
require.NotContains(t, res.Header.Get(httpmw.HSTSHeader), fmt.Sprintf("max-age=%d", int64(httpmw.HSTSMaxAge)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The false case should probably check it contains the empty string ""

return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if hsts {
w.Header().Set(HSTSHeader, fmt.Sprintf("max-age=%d", int64(HSTSMaxAge)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make the value of this header configurable by the flag? With "true" defaulting to our max-age? Seems like there's a swath of options that we could set, but might not have the best idea when it's appropriate.

cli/start.go Outdated
@@ -334,6 +338,8 @@ func start() *cobra.Command {
cliflag.BoolVarP(root.Flags(), &useTunnel, "tunnel", "", "CODER_DEV_TUNNEL", true, "Serve dev mode through a Cloudflare Tunnel for easy setup")
_ = root.Flags().MarkHidden("tunnel")
cliflag.BoolVarP(root.Flags(), &traceDatadog, "trace-datadog", "", "CODER_TRACE_DATADOG", false, "Send tracing data to a datadog agent")
cliflag.BoolVarP(root.Flags(), &hsts, "hsts", "", "CODER_HSTS", false, "Set the 'strict-transport-security' header on http responses")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HSTS and struct-transport-security confuse me a bit. With my other comment, could we call this:

CODER_STRICT_TRANSPORT_SECURITY

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had this originally and thought it may be too long and changed it lol.

cli/start.go Outdated
@@ -334,6 +338,8 @@ func start() *cobra.Command {
cliflag.BoolVarP(root.Flags(), &useTunnel, "tunnel", "", "CODER_DEV_TUNNEL", true, "Serve dev mode through a Cloudflare Tunnel for easy setup")
_ = root.Flags().MarkHidden("tunnel")
cliflag.BoolVarP(root.Flags(), &traceDatadog, "trace-datadog", "", "CODER_TRACE_DATADOG", false, "Send tracing data to a datadog agent")
cliflag.BoolVarP(root.Flags(), &hsts, "hsts", "", "CODER_HSTS", false, "Set the 'strict-transport-security' header on http responses")
cliflag.BoolVarP(root.Flags(), &secureCookie, "secure-cookie", "", "CODER_SECURE_COOKIE", false, "Set the 'Secure' property on browser session cookies")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our description would be more helpful if it linked to MDN or something. As a user, I'd just Google that right away!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do in the dash but I wanted to keep these help docs slim. But I'm down to add it if we don't mind the verbosity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally kind of like just letting people google the header? It's super easy to grok

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, I'd adjust these to use " like other parameter help sections do for consistency. I wouldn't call this Set either. In other commands we use Specifies, which I think more accurately describes it (because it's a bool).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SecureCookie might be worth explicitly stating SecureAuthCookie instead. That way there's no ambiguity.

Comment on lines 11 to 12

"github.com/coder/coder/coderd/httpmw"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

close this import gap

@f0ssel
Copy link
Contributor Author

f0ssel commented Mar 30, 2022

Updated with the feedback. I think we should keep max-age unconfigurable for now, it's really easy to add it later as a envflag and I'm not convinced anyone will ever ask for it.

@f0ssel f0ssel requested review from Emyrk and kylecarbs March 30, 2022 22:24
@f0ssel f0ssel force-pushed the f0ssel/security branch 3 times, most recently from b7eda3e to 803313d Compare March 30, 2022 22:31
Comment on lines 10 to 12
"github.com/stretchr/testify/require"

"github.com/coder/coder/coderd/httpmw"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait still need to close this gap right? Github imports have no newline gaps?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, they do for internal imports.

Copy link
Member

@kylecarbs kylecarbs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to rename the hsts*.go files before merge. I added another comment about renaming secureCookie to secureAuthCookie instead, which I think makes it a bit more explicit.

I don't think we should export objects just for testing. Apart from that, looks good to me!


const (
StrictTransportSecurityHeader = "Strict-Transport-Security"
StrictTransportSecurityMaxAge = time.Hour * 24 * 365 // 1 year
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we just check for the existence of the header, we can remove these as exports.

Exporting just for tests is bad practice from my perspective. If a customer were to depend on these constant values and they changed randomly, it would break their stuff. Tests should run against the API just like a customer would.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a lint rule that requires that tests use httmw_test package which then forces you to export. I'll figure out how to disable that linter?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should disable that lint rule, as it prevents us from creating good package scopes.

I think it's totally reasonable to test unexported vars/consts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should disable that linter, I think we should duplicate the values!

Customers testing against our APIs likely won't use our constants, they'll probably use the values.

I intentionally enabled that linter to avoid package bloat and poor decomposition.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/maratori/testpackage

I recommend you read the attached posts (there are like four of them) describing its intention. If you still disagree, I'm happy to reconsider.

Copy link
Contributor Author

@f0ssel f0ssel Mar 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally see the value, and get the philosophy forcing tests to use the public api. Also I'm behind the duplicate values thing too, I considered that solution as well.

I do however think when it comes to testing I'd rather have both public and private functional testing as an option if we want it. I see value in the forcing function but think it may be too heavy handed to say "no private function testing".

I'll dupe the values for this PR and keep the lint rule for now.

Copy link
Contributor Author

@f0ssel f0ssel Mar 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the "export_test" example (the 4th link) is a pretty bad pattern. I wouldn't want to have a test file dedicated to bridging the gap and avoid the linter, seems super confusing and fighting the toolchain.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah we can do XXX_internal_test.go. So I'm good with the linter if we have the option to go around it 👍.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the "export_test" example (the 4th link) is a pretty bad pattern. I wouldn't want to have a test file dedicated to bridging the gap and avoid the linter, seems super confusing and fighting the toolchain.

Yea, I was not a fan of that either. I think with the _internal_ option for naming though, we can still write internal tests. I am fine with that.

Copy link
Member

@kylecarbs kylecarbs Mar 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly! There are of course edge-cases, but the linting rule optimizes for 95% of cases. In the 5% where it doesn't fit, the _internal_ pattern provides well.

@f0ssel f0ssel force-pushed the f0ssel/security branch 2 times, most recently from 96bcf77 to 368ead5 Compare March 31, 2022 02:13
Comment on lines 9 to 13
const (
strictTransportSecurityHeader = "Strict-Transport-Security"
strictTransportSecurityMaxAge = time.Hour * 24 * 365 // 1 year
)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are just used in the one place, I don't think they should be constants.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consts are more self documenting (variable name) and cleaner code to read imo. I feel like I was told never to use magic numbers like day 1 of programming and have never looked back.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it anyways to get this pr merged....

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can leave em' as consts that's just a minor nit.

Comment on lines 16 to 19
const (
strictTransportSecurityHeader = "Strict-Transport-Security"
strictTransportSecurityMaxAge = time.Hour * 24 * 365
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should probably be in the test function scope. If they aren't used globally between tests, it could be confusing.

@f0ssel f0ssel requested a review from kylecarbs March 31, 2022 15:17
Copy link
Member

@kylecarbs kylecarbs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! One comment is just a question, not a change.

coderd/coderd.go Outdated
r.Use(chitrace.Middleware())
r.Use(
chitrace.Middleware(),
httpmw.StrictTransportSecurity(api.StrictTransportSecurity),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed for static assets as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Emyrk ? I don't know but my guess is "no" because it will already be set on the browser after the first api call and also static assets would never have sensitive data.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HSTS should be on static, but I think it might not really matter as you only need to hit 1 HSTS header for it "take effect". Every HSTS header after is redundant

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably move it outside of there then, just to confirm properly.

@kylecarbs
Copy link
Member

We should update the title too- since we renamed HSTS to StrictTransportSecurity.

@f0ssel f0ssel changed the title feat: Add HSTS and secure cookie options feat: Add strict transport security and secure cookie options Mar 31, 2022
kylecarbs added a commit that referenced this pull request Mar 31, 2022
A discussion (linked below) was had that touched on why this linter
is enabled. To avoid losing that history, adding the comment inline with
our linting rules can avoid duplicating this discussion!

#741 (comment)
@ammario
Copy link
Member

ammario commented Mar 31, 2022

HSTS should be configured by a load balancer or proxy, not the end application. It's a niche requirement and readily configurable by almost every reverse proxy.

Not even GitLab has built in HSTS. Instead, they suggest users configure nginx.

Copy link
Member

@ammario ammario left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(See my comment)

@f0ssel
Copy link
Contributor Author

f0ssel commented Mar 31, 2022

@ammario can I link this comment in 6 months when an enterprise customer / our PM asks for this again like they did in v1?

@f0ssel f0ssel requested review from Emyrk, kylecarbs and ammario March 31, 2022 17:16
@ammario
Copy link
Member

ammario commented Mar 31, 2022

@f0ssel of course :)

@f0ssel f0ssel dismissed stale reviews from ammario and Emyrk March 31, 2022 17:30

deleted the code

@f0ssel f0ssel merged commit 0d53795 into main Mar 31, 2022
@f0ssel f0ssel deleted the f0ssel/security branch March 31, 2022 17:31
kylecarbs added a commit that referenced this pull request Mar 31, 2022
A discussion (linked below) was had that touched on why this linter
is enabled. To avoid losing that history, adding the comment inline with
our linting rules can avoid duplicating this discussion!

#741 (comment)
@misskniss misskniss added this to the V2 Beta milestone May 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants