-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Forwarded proto #2519
Forwarded proto #2519
Conversation
753c56b
to
9443a35
Compare
Thanks for the work on this. It's top on my list for early next week. I appreciate the patience. |
core/lib/src/request/from_request.rs
Outdated
/// * **Protocol** | ||
/// | ||
/// Extracts the protocol of the incoming request as a [`Protocol`] via | ||
/// [`Request::proxy_proto()`] (HTTP or HTTPS). If the used protocol is not | ||
/// known, the request is forwarded. | ||
/// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems problematic. I would expect such a guard to succeed 99.9% of the time, given the name of the type. But this will only work when the header is set. Is there a benefit to exposing this? If we really want to do this, we should change the name to something less general, like ForwardedProtocol
, or something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While working on this I thought I would need this, but I realised that in the end we didn't but it might be useful to someone else. As it's trivial to implement, I will just remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decided to keep it, as the tests I added are using it, but I renamed it to ForwardedProtocol
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This discussion still stands, unless keeping it is now deemed acceptable. Also see #2519 (comment)
My initial inclination (beside "awesome job!") is that this is doing far too much. I would have expected the implementation to:
I'd like to change this implementation such that we get there. In particular, I really dislike I'm not convinced by the name |
I renamed everything to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After some consideration, I feel like we should really call it ProxyProtocol
or ProxyProto
, as it is quite literally the protocol that the proxy is using to communicate. Similarly, the config parameter should be called proxy_proto_header
or similar, and the rest of the code should be adjusted accordingly.
With respect to how the CookieJar
determines whether it's in a secure context, I really feel like we need a better approach. Right now, you have to call set_proxy_secure
everywhere that we create a Request
. But this is really error prone. It's clear that CookieJar
needs to have everything it needs when it's created. This way, we can't forget to set something later.
The ideal would be to have CookieJar::new()
ask for an &Request
. Unfortunately this would mean that Request
would need to hold a borrow to itself (Request
contains RequestState
which contains CookieJar
), which is no fun. And it also wouldn't really work since we don't know the headers the request will have until later, in the case of LocalRequest
. I think the next best thing to do is to:
- Remove the
set_()
method(s). - Change the code so we have a new
request.context_is_likely_secure()
method. - Have a single constructor for
CookieJar
that takes both the&Config
(which we need for the secret key) and a newsecure_context: bool
which will be set toconfig.tls_enabled() || request.context_is_likely_secure()
. When we don't have the final request, we can't really do anything. But we should changeRequest::from_hyp()
so that it actually callsCookieJar::new()
instead of adding to the existing cookie jar. At least then we're not mutating something. TheCookieJar
created inRequestState
will just have the contextconfig.tls_enabled()
, which is correct but suboptimal.
I'll give this a second look later today to see if there's something more robust we can do.
85b7f48
to
ab072f4
Compare
Implement your suggestions, except for the |
These latest changes look great! Pending consistently using the |
Unexpectedly had some time today. Is this more or less what you had in mind? I also optimised the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome. These suggestions are largely for me. I'll make the changes and get this committed. Thank you!
Thanks! I see now that I forgot to change some comments to the new wording. Do you want me to do any of those changes? I did one more suggestion for a mistake of mine in the docs |
6ac94f5
to
e8d326c
Compare
Thanks for noticing that! Caught it in a commit I just pushed that addresses all of the issues I've brought up. The original ideas and implementation were buggy as well as they didn't react correctly if the header was set after the request was received, e.g., by a request fairing. This commit fixes this by going through the existing header cache busting infra. This work brought to light a different issue, however: any cookies added to the jar before such a header is set won't be marked "secure". This was the case in previous iterations as well, of course, it's just that this implementation made the issue obvious. It's unclear to me whether this is a big concern or not. Certainly, what's we're doing now is better than what we did before. However, we might also be providing a false sense of security that has edge cases, which might be worse. The potential paths forward I see are:
I'm inclined to go with 4. It's really unlikely this edge case will be hit, and even more unlikely that it will cause any sort of security issue. But I can be convinced to go with 3. |
Oh, I should say, the following things still need to be done:
|
Thanks for the cleanup, it’d gotten a bit of mess after the back and forth renaming. I really like what you did with the So, to get to the best decision, I’d like to reiterate the idea behind the PR. In order to be able to add one more secure default to cookies set by Rocket running behind a proxy, the Hope this helps in reaching a conclusion. |
@SergioBenitez So I bought about it some more and, unless I understand request fairings wrongly, I do think inspecting the header before it reaches any request fairing (i.e. in Let me know if you have any more concerns so we can finish this PR |
@arjentz I'm willing to move forward with merging this PR. Can you address the final remaining issues outlined in #2519 (comment)? |
Thanks! I will add that, hopefully somewhere this week, otherwise next week. Do you agree that moving the logic back to |
Not as of now. Can you explain why you think this is the way to go? As far as I see it, doing this has the following immediate drawbacks:
The only drawback to the current approach is an additional string comparison when a header is set.
This the case with the either approach. |
@SergioBenitez did you see my comment here? I used the following code to test:
But it doesn't seem to work now (
Even though it did before (
|
Let me rebase on |
e8d326c
to
bfe6ece
Compare
Ah, right, yes, you're absolutely right. In the |
Note that this can only really occur in local testing, where the jar is retrieved manually before setting a header. Otherwise, we only return a |
Okay, I've fixed the issue while also simplifying the impl. Please take a look and let me know what you think. Also check out the unresolved review items as well as my commentary for what's still left to be done. |
Thanks for taking the time. I really like how you simplified it, especially the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor changes here, nothing interesting. I think we're at the finish line!
For the documentation on when context_is_likely_secure()
affects defaults, we need to say that the Secure
flag is set as long as it's not explicitly set to false
. It's already mentioned that this is a "default", but I think being explicit here is worthwhile.
site/guide/9-configuration.md
Outdated
| `max_blocking`* | `usize` | Limit on threads to start for blocking tasks. | `512` | | ||
| `ident` | `string`, `false` | If and how to identify via the `Server` header. | `"Rocket"` | | ||
| `ip_header` | `string`, `false` | IP header to inspect to get [client's real IP]. | `"X-Real-IP"` | | ||
| `proxy_proto_header` | `string`, `false` | Header identifying client to proxy protocol. | `None` | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should link ProxyProto
the way LogLevel
does, for example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I linked proxy_proto
the way ip_header
does. I figured ProxyProto
itself is not particularly useful here, as it is not used as the setting itself.
79ee335
to
38603f1
Compare
Rebased the branch and added the configuration entry. Happy to iterate on that a bit more if you think it needs that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're set functionality wise. The docs need a bit of work. I'll fix them up, push a commit, and get this merged in a bit.
#[get("/")] | ||
fn inspect_proto(proto: Option<ProxyProto>) -> String { | ||
proto | ||
.map(|proto| match proto { | ||
ProxyProto::Http => "http".to_owned(), | ||
ProxyProto::Https => "https".to_owned(), | ||
ProxyProto::Unknown(s) => s.to_string(), | ||
}) | ||
.unwrap_or("<none>".to_owned()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#[get("/")] | |
fn inspect_proto(proto: Option<ProxyProto>) -> String { | |
proto | |
.map(|proto| match proto { | |
ProxyProto::Http => "http".to_owned(), | |
ProxyProto::Https => "https".to_owned(), | |
ProxyProto::Unknown(s) => s.to_string(), | |
}) | |
.unwrap_or("<none>".to_owned()) | |
} | |
#[get("/")] | |
fn inspect_proto(proto: ProxyProto) -> String { | |
proto.to_string() | |
} |
And then adjust the tests so they look for a 500
when there is no such header.
assert_eq!(response.into_string().unwrap(), "https"); | ||
|
||
let response = client.get("/").dispatch(); | ||
assert_eq!(response.into_string().unwrap(), "<none>"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
e.g.:
assert_eq!(response.into_string().unwrap(), "<none>"); | |
assert_eq!(response.status(), Status::InternalServerError); |
site/guide/9-configuration.md
Outdated
ip_header = "X-Real-IP" # set to `false` or `None` to disable | ||
proxy_proto_header = `false` # set to `false` or `None` to disable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is saying None
correct here? This implies you can write:
ip_header = None
proxy_proto_header = None
And it'll disable the header. But that isn't the case.
ip_header = "X-Real-IP" # set to `false` or `None` to disable | |
proxy_proto_header = `false` # set to `false` or `None` to disable | |
ip_header = "X-Real-IP" # set to `false` to disable | |
proxy_proto_header = `false` # set to `false` to disable |
site/guide/9-configuration.md
Outdated
If Rocket is running behind a reverse proxy that terminates TLS, it is useful to | ||
know whether the original connection was made securely. Therefore, Rocket offers | ||
the option to configure a `proxy_proto_header` that is used to determine if the | ||
request is handled in a secure context. The outcome is available via | ||
[`Request::context_is_likely_secure()`] and used to set cookies' secure flag by | ||
default. To enable this behaviour, configure the header as set by your reverse | ||
proxy. For example: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, we should be more specific: why is it useful? I'd suggest inverting this paragraph to something like:
If Rocket is running behind a reverse proxy that terminates TLS, it is useful to | |
know whether the original connection was made securely. Therefore, Rocket offers | |
the option to configure a `proxy_proto_header` that is used to determine if the | |
request is handled in a secure context. The outcome is available via | |
[`Request::context_is_likely_secure()`] and used to set cookies' secure flag by | |
default. To enable this behaviour, configure the header as set by your reverse | |
proxy. For example: | |
The `proxy_proto_header` configuration parameter allows Rocket applications to | |
determine when and if a client's initial connection was likely made in a | |
secure context by examining the header with the configured name. The | |
header's value is parsed into a [`ProxyProto`] value and retrievable via | |
[`Request::proxy_proto()`]. | |
That value is in-turn inspected to determine if the initial protocol was secure | |
(i.e, over TLS) and the result made available via | |
[`Request::context_is_likely_secure()`]. The value returned by this method | |
influences cookie defaults. In particular, if the method returns `true` (the | |
request context is likely secure) secure, the cookie `Secure` cookie flag is set | |
by default. |
Co-authored-by: Sergio Benitez <sb@sergio.bz>
997b5b9
to
1ea9421
Compare
PR for #2425. Some notes:
Protocol
is based largely onProtocolName
from https://github.com/hyperium/hyper-old-types/blob/0426d7ac64c955af6327279c595168c39e7b1415/src/header/common/upgrade.rs#L84ip_header::deserialize
tohttp_header::deserialize
to be able to re-use itx-real-ip
header is handled: 9377af5CookieJar
has an extra fieldproxy_secure
that is set internally when it makes sense to do soRequest
has aproxy_proto
andproxy_secure
method used internallyFromRequest
is implemented forProtocol
proto_header
defaults toNone
/false