vpn-user-portal
vpn-user-portal copied to clipboard
Allow for proxied connections
This change adds support for running the user portal behind a proxy server. In my case, I use an nginx frontend proxy server for both TLS encryption and HA purposes (i.e. routing incoming calls to a working backend user portal service).
Supported headers for proxying, as used in the nginx config:
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $request_port;
Unit tests have been added.
Thanks!
As this request header is using HTTP_
it could be directly sent by the client as well, and not necessarily by a (reverse) proxy. How can this be prevented? I'm not sure if this introduces a vulnerability in the no-proxy scenario, but it is probably smart not to allow for any wiggle room...
Would it not be simpler to convert Apache's VirtualHost
section from HTTPS to HTTP, but keep using ServerName https://vpn.example.org:443
? Or does this not work? Haven't tested it... Or did you get rid of Apache completely?
In addition, perhaps this proxy logic could be put behind an environmental variable set by Apache using e.g. SetEnv BEHIND_PROXY "yes"
or something?
The proxy request headers could be spoofed yes. But what would happen, is that this would result in a different _user_pass_auth_redirect_to
location, which is eventually handed back to the web browser, which handles the redirect. So the web browser would be able to let the web browser itself do a wrong redirect.
Note that it would be considerably easier for a client to simply modify that hidden field in the login form directly :-)
The other impact that I see, is that the filename for the downloaded ovpn config would be changed. That's not really an attack vector either. For me it's even part of the solution, because this way the downloaded config carries the correct hostname in its file name.
I do agree that not allowing any wiggle room is a good thing, because in security, one might easily overlook certain impact. For this, I can extend the code, by adding a configuration option for telling the user portal what proxy IP-addresses can be trusted. That is normally the way in which a proxy setup is secured, since then only the proxy or proxies can be responsible for the extra request headers and no other internal hosts can pretend to be proxies. I'll add this configuration option to the code.
Would it not be simpler to convert Apache's VirtualHost section from HTTPS to HTTP, but keep using ServerName https://vpn.example.org:443?
Protocol and port are not part of the ServerName. Therefore using vpn.example.org
would fix part of the issue, but not all of it. With the proxy setup, the Apache service could be using any protocol and port, and the service would not be able to tell what protocol and port the user is actually using. This is especially true when using for example Docker containers for running the portal, where the exposed service port is even random.
Or did you get rid of Apache completely?
No, Apache is still running for serving the user portal website. To clarify, my current setup (using the development EduVPN environment) looks like this:
So the EduVPN node has the OpenVPN and WireGuard ports directly exposed to the internet via port forwarding, while the Apache webserver is accessed through the nginx reverse proxy.
I added configuration options for the reverse proxy setup.
-
IP addresses/ranges that are allowed to reverse proxy the user portal can and must now be configured in the config file. When the requesting IP address is not in the proxy list, then the standard functionality for lookup up scheme, host and port is used.
-
Additionally, I added some options to allow configuration of the specific headers that the reverse proxy uses to pass on scheme, host and port to the EduVPN server. This allows for easier integration with reverse proxies that don't use the default HTTP header names.
On my system, I now have a working configuration, using:
<?php
$baseConfig = include __DIR__.'/config.php.example';
$localConfig = [
'HttpRequest' => [
'proxyList' => [
'192.168.100.171',
'192.168.100.172',
'192.168.100.173'
]
],
'adminUserIdList' => ['admin'],
'vpnCaPath' => '/root/Projects/eduVPN-v3/vpn-ca/vpn-ca',
'ProfileList' => [
[
// Yada yada, the usual ...
],
],
];
return array_merge($baseConfig, $localConfig);
The three IP-addresses are the source IP-addresses that are used by my reverse proxy setup. I could also have used CIDR notation (thanks to the IP address tooling already available 👍)
The relevant part of my nginx configuration looks somewhat like this:
# Currently only one backend server, but the production setup would have two or three.
upstream upstream-vpn {
ip_hash;
server 192.168.100.103:8082;
}
server {
listen 443 ssl http2;
server_name my.public.hostname.nl;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
location / {
proxy_pass http://upstream-vpn;
allow all;
proxy_http_version 1.1;
proxy_pass_request_headers on;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $request_port;
}
}
Just out of curiosity. Is there any benefit to your setup when you could also use sniproxy/sslh, or similar? Is your reverse proxy faster at handling TLS?
I'm not saying a valid case can't be made for your scenario, for example if you have a hardware TLS key store or something running on your (reverse) proxy protecting the TLS keys, but it seems in most (all?) scenarios using e.g. sniproxy/sslh would be much easier and secure.
Maybe "offloading" wasn't the correct term to use her. It's not really about the reverse proxy being faster at handling TLS. Still, it is very fast though, and because the function "TLS" is on a separate node, it would allow for horizontal scaling that specific function, when required.
It is partly about offloading TLS complexity to the reverse proxy hosts. My nginx nodes know how to grab a letsencrypt certificate, and how to handle the TLS connections. My backend user portal server doesn't do TLS at all, and only has to care about serving the correct web responses. This separation of concerns makes the setup of the eduVPN node simpler. Of course, this should only be done when the backend network can be trusted. When the network is untrusted, then the reverse proxy could still use HTTPS to talk to the eduVPN node. This backend communication could be implemented using self-signed certificates.
My reverse proxy hosts also handle incoming requests for other services on my network. They are not dedicated eduVPN reverse proxies. I generate the config for these proxies from a Nomad+Consul cluster, making nginx a core component of my setup. So there's another simple reason why reverse proxying instead of doing something like sniproxy makes sense for me: not having to overthrow that part of the architecture.
What reverse proxying also allows me to do, is that I can easily setup a new backend server version on a new host, and then route some test traffic to that new backend. When all is okay, the switch to the new version can be made by proxying all traffic to the new host (keywords: canary releases, green/blue deployment). While sniproxy is a cool application, I haven't been able to use it in such way.
Another use case that I'm planning for with this setup, is to make nginx handle high availability. The documentation tells me to use keepalived for HA purposes. That will work and it is a type of setup that I use too. However, when possible, my personal preference is to use nginx or haproxy, and letting these do active status checks to see what backend or backends can be used. An important reason for this preference, is that I can serve a clean "service is down, sorry" page when no backend is operational, while the keepalived route would result in something like a connection refused error.
Other use cases that are possible with the reverse proxy and not with sniproxy are about implementing additional authorization rules and checks for incoming requests. One example of this would be to setup nginx as a web application firewall.
This would also be possible by adding mod_security
to the Apache instance, however by doing this on the reverse proxy, the backend webserver is fully isolated from direct internet access, adding an extra layer of security.
Quite a story, but I think these are the main reasons for wanting to make use of the reverse proxy setup, instead of using a SNI-based proxy.
Protocol and port are not part of the ServerName. Therefore using vpn.example.org would fix part of the issue, but not all of it. With the proxy setup, the Apache service could be using any protocol and port, and the service would not be able to tell what protocol and port the user is actually using. This is especially true when using for example Docker containers for running the portal, where the exposed service port is even random.
https://httpd.apache.org/docs/2.4/mod/core.html#servername
Sometimes, the server runs behind a device that processes SSL, such as a reverse proxy, load balancer or SSL offload appliance. When this is the case, specify the https:// scheme and the port number to which the clients connect in the ServerName directive to make sure that the server generates the correct self-referential URLs.
So from what I read here, this would work as-is without requiring this PR... or what is still missing in this case?
I did not know about the ServerName
syntax with a scheme included. I saw this one is new since Apache 2.2. I only knew about the Apache 2.0 version without scheme, so thanks for that :-)
Based on this, when using Apache2 as the webserver, this can work (in fact, I gave it a try and the required request parameters do end up in the $_SERVER
globals).
However, the drawback of this solution is that it is not webserver-agnostic. When using a different webserver application for serving the user portal, proxied connections will fail.
For me, this already happens when I'm launching the development environment webserver (which also runs through the reverse proxy), using php -S 0.0.0.0:8082 -t web
.
So the problem has been resolved and there's no longer the need for this PR?
The PHP built-in web server doesn't work anyway because of lack of /.well-known/vpn-user-portal
handling, but there might be options using a "routing script". Other web servers might support similar configuration directives like ServerName
in Apache?
If other web servers really can't be made to work, note: we do not support those (yet) anyway, a better approach might be a "forceServerName" or something configuration directive, not needing to deal with proxy stuff at all in the application.
The PHP built-in web server doesn't work anyway because of lack of /.well-known/vpn-user-portal handling, but there might be options using a "routing script".
I was already working on that. I just sent in a PR for it: https://github.com/eduvpn/vpn-user-portal/pull/204
there's no longer the need for this PR?
I do think there is still a need for it.
Having to set the ServerName
or some forceServerName
option would tightly couple the configuration of the backend eduVPN service to data that only needs to be bound to the frontend service, i.e. the reverse proxy.
Additionally, by focusing on ServerName
as the solution, the application is coupled to the webserver technology.
Like any kind of coupling, this makes aspects of the setup more complex and harder to manage. When no good solution exists for such coupling issue, then one has to bite the bullet. However, the proposal of this PR is to implement a widely known and used methodology to work with web services behind a reverse proxy.
if other web servers really can't be made to work, note: we do not support those (yet)
That's the thing. By using the PR, it is actually very easy to get other webservers to work. I really don't mind if nginx isn't officially supported, as long as the system isn't actively working against me when I try to use it. That is currently not the case.
That's the thing. By using the PR, it is actually very easy to get other webservers to work. I really don't mind if nginx isn't officially supported, as long as the system isn't actively working against me when I try to use it. That is currently not the case.
The thing is, Your immediate problem is solved. It is a lot of code to implement something that can be done much simpler and secure by leveraging the web server directly, i.e. through a one liner configuration option. That has a very clear benefit... Just being pragmatic here.
Once this feature is needed to support other web servers and/or proxy scenarios, in a production environment, and they can't be configured with a directive similarly to ServerName
, we can revisit this.
It is a lot of code to implement something that can be done much simpler and secure
"That it is a lot of code" is extremely subjective. Most of the changes are about updating the unit tests and documenting+handling the required configuration options. I would say the actual functional code is in fact very limited: check if the requesting host is in the list of proxies, if yes then fetch host, scheme and port from alternative $_SERVER
fields.
"Much simpler" is subjective as well, or at least highly opinionated. In my deployment scenario and use case, proper proxy support really makes the deployment objectively much simpler.
As for "secure", I would really like to understand how the proposed code change is causing insecurity. I do know about the eduVPN policy of limiting the codebase for security reasons, but that should not be a wildcard for dismissing PRs.
Many aspects for eduVPN/Let's Connect! are highly opinionated. That's what keeps it relatively small and simple. Convention over configuration and writing the least amount of code possible to obtain the required functionality. In this scenario the indicated problem which this PR aims to solve can be resolved by changing 1 line in the web server configuration, which needs to be modified anyway for your particular proxy scenario.
Ahead of time "optimization" for a possible future scenario where someone does to work to support additional web servers are not necessarily effective. It is better to tackle that when the time comes as one cannot possibly foresee all scenarios and additional changes (if any!) might be required anyway.
PRs are not accepted because they are there. They need to pass a (yes, subjective!) filter whether or not the benefits outweigh the added (maintenance/potential security impact) costs of merging them and solve any immediate problem for which no other solution is possible.
by changing 1 line in the web server configuration, which needs to be modified anyway for your particular proxy scenario.
Of course the Apache config will need some modification. That is a given. However, the Apache server does not need to know at all what public URL or URLs are used to access the service. Only the front end needs to know this. Therefore my point is that this specific information does not need to be in the Apache config.
Ideally, I can do things like routing new-vpn.mydomain.nl
to a new instance and (after testing), assign vpn.mydomain.nl
to this new instance, without ever having to touch the backend service configuration.
Another use case-specific application for me, is that I am providing VPN-services for a couple of non profit organizations, each with their own network profile setup.
While I can tell them to use vpn.mymaindomain.nl
and there select the correct profile from the list of profiles, I'd rather use a form of virtual hosting, giving them vpn.theirorganizationname.nl
as the entrypoint instead, allowing me to automatically select the correct profile and apply some organization-specific styling to the portal.
Yes, this is probably all possible when using the ServerName
trickery, but I will need to setup a virtualhost for every frontend URL that I am using, so I can configure multiple ServerNames to let all entrypoints use the correct redirect URLs. That is a lot more than 1 line of config.
You are missing changing the hostName
field in the profile configuration:
https://github.com/eduvpn/documentation/blob/v3/CHANGE_HOSTNAME.md
Let's leave it here for now and revisit when there's no other way than to implement something like your proposal.
You are missing changing the
hostName
field in the profile configuration:
You're assuming things. I am not missing that at all. Changing that is only needed when is it not set dynamically and I use
'hostName' => $_SERVER['HTTP_HOST']
in the config for that.
I gave up on this and switched to OpenVPN access server for my needs. So I'll close this PR now as "won't fix".