Imagine, not entirely hypothetically, that you have a client that needs you to work on multiple systems only accessible via https (where the domain name needs to match), all located behind the client's firewall. Further suppose that the only access they can provide to their network is ssh to a bastion host -- even while located in their physical office, only "guest" network access to the Internet is available. Assume, also not entirely hypothetically, that they have no VPN server. Finally assume, again not entirely hypothetically, that no software can be installed on the bastion host, and that it runs Ubuntu Linux 12.04 LTS (hey, there is at least a month of maintenance support left for that version...).
In this situation there are a few reasonable approaches that preserve the browser's view of the domain name:
an outgoing (forward) web proxy, supporting
CONNECT
transparent redirection of outgoing TCP connections to a proxy
tricks with DNS resolution (eg
/etc/hosts
), possibly combined with one of the above.
I did briefly experiment with transparent redirection of the outgoing
TCP connections (which works well on Linux: iptables -t nat -A
OUTPUT ...
), but since I was working from a Mac OS X desktop system
it was more complicated (Mac OS X uses pf
, and pf.conf
can
include rdr
statements to redirect
packets,
but intercepting locally originated traffic involves multiple steps
and seems somewhat fragile and was not working reliably for me).
Instead I went looking for a way to implement a web forward proxy,
on the bastion host. Since I could not install software on the bastion
host, I needed to find something already installed which could be
repurposed to be a forward proxy. Fortunately it turned out that the
bastion host had been installed with apache2
(2.2.2), to support
another role of the host (remote access to monitoring output). I then
needed a configuration that could use apache2
in a forward proxy mode.
Apache 2.2 provides a forward proxy feature through
mod_proxy
, but
it is definitely something you want to secure carefully as the
documentation repeatedly warns. In addition the bastion host naturally
was firewalled from the Internet to allow only certain ports to be
reached directly, including ssh
, so simply running a web proxy on some
port on an Internet reachable IP was never an option.
To solve both of these problems I created a configuration to run
another instance of Apache 2.2, with mod_proxy
enabled in forward
proxy mode, listening on localhost, that could be reached only via
ssh port forward (based on examples
from the
Internet).
This involved creating a custom configuration to run Apache 2.2 with:
Listen 127.0.0.1:3128
# Access control functionality
LoadModule authz_host_module /usr/lib/apache2/modules/mod_authz_host.so
# Proxy functionality
LoadModule proxy_module /usr/lib/apache2/modules/mod_proxy.so
LoadModule proxy_http_module /usr/lib/apache2/modules/mod_proxy_http.so
LoadModule proxy_connect_module /usr/lib/apache2/modules/mod_proxy_connect.so
# Logging
LogFormat "%h %l %u %t \"%r\" %s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogLevel warn
ErrorLog logs/error.log
CustomLog logs/access.log combined
# PID file
PidFile logs/apache2.pid
# Forward Proxy
ProxyRequests On
# Allow CONNECT (and thus HTTPS) to additional ports
AllowCONNECT 443 563 8080
as well as a section (in angle brackets) to match "Proxy *
", which
limited access to the proxy to localhost:
Order Deny,Allow
Deny from all
Allow from 127.0.0.1
(Full Apache 2.2 example config.)
Note that the above configuration will work only with Apache 2.2 (or earlier);
the configuration for the Apache 2.4
mod_proxy
, and
other Apache features, changed significantly, particularly around
authorization. (ETA, 2017-04-03: See end of post for update with
Apache 2.4 configuration.)
The key features in the above configuration is that it loads the
modules needed for HTTP/HTTPS proxying, and IP authentication, and
then listens on 127.0.0.1:3128
for connections and treats those
connections as forward proxy connections -- thanks to ProxyRequests
On
and the "Proxy *
" section. The "Proxy *
" section is intended
to allow access only from localhost itself. It may be useful to
also add user/password authentication to the proxy use.
Put the config in a directory, and then make a "logs" directory:
cd ....
mkdir logs
And create a simple wrapper script to start up the proxy on the bastion
host (eg, called "go
"):
#! /bin/sh
# Run Apache in forward proxy mode to be reached via ssh tunnel
#
exec apache2 -d ${PWD} -f ${PWD}/apache-2.2-forward-proxy.conf -E /dev/stderr
Then the proxy can be started up when needed with:
cd ....
./go
and will run in the background. When run, you should see something
listening on TCP/3128 on localhost on the bastion host, eg with
netstat -na | grep 3128
:
tcp 0 0 127.0.0.1:3128 0.0.0.0:* LISTEN
From there, the next step is to ssh into the bastion host with a port
forward that allows you to reach 127.0.0.1:3128
:
ssh -L 3128:127.0.0.1:3128 -o ExitOnForwardFailure=yes -4 -N -f HOST
which should cause ssh to listen on our local (desktop) system, here
also on port 3128
, so netstat -na | grep 3128
on the local system
should also show:
tcp 0 0 127.0.0.1:3128 0.0.0.0:* LISTEN
The final step is to set the proxy configuration of your web browser
to use a web proxy at 127.0.0.1 on port 3128, so that your
web browser will use the proxy for HTTP/HTTPS connections. For Safari
this can be done with Safari -> Preferences... -> Advanced ->
Proxies: Change Settings..., which will open the system wide proxy
settings. You need to change both "Web Proxy (HTTP)" and "Secure web
proxy (HTTPS)" for this to work in most cases, ticking them and then
setting the "Web Proxy Server" to "127.0.0.1
" and the port to "3128
".
After that your web browsing should automatically go through the
127.0.0.1:3128 connection on your desktop system, via the ssh port
forward to Apache 2.2/mod_proxy
on the bastion host, and then
CONNECT
out to the desired system, giving a transparent HTTPS
connection so that TLS certificate validation will just work.
To access from a command line client it is typically useful to set:
http_proxy=https://127.0.0.1:3128/
https_proxy=https://127.0.0.1:3128/
export http_proxy https_proxy
because most client libraries will look for those (lowercase) environment variables when deciding how to make the connection. This enables using, eg, web REST/JSON APIs from Python, with the proxy.
If the bastion host is restarted then the proxy will have to be manually restarted (as above); but if it is running on a legacy Linux install chances are that the client will be unwilling to reboot the host regularly due to being uncertain if it will boot up cleanly with everything needed running. I got through the entire project without having to restart the proxy.
The main catch with this configuration is that because the Safari
proxy setting are system wide, they affect all traffic including
things like iTunes. I worked around that by using another browser
which had its own network connection settings (Firefox) for regular
web browsing, and turning the proxy settings on and off as I needed
them to work on that project. If this were to be a semi-permanent
solution it might either be better to use a browser with its own
proxy settings (eg, Firefox; or one in a VM) dedicated to the
project, and leave the system wide settings alone. Or perhaps to
create a Proxy
Auto-Config
(.pac
) file which redirected only certain URLs to the proxy --
it is possible to arrange to load those from a local file instead
of a web URL.
ETA, 2017-04-03: Inevitably, given that the support for Ubuntu 12.04 LTS runs out this month, the client upgraded to the bastion host Ubuntu 14.04 LTS (but not Ubuntu 16.04 LTS; presumably due to being a smaller jump). This brings in Apache 2.4 instead of Apache 2.2, which brings in a non-trivial changes in configuration syntax.
The incremental differences are relatively small though, for a basic config. You need to load a couple of additional modules:
# Apache 2.4 worker
LoadModule mpm_worker_module /usr/lib/apache2/modules/mod_mpm_worker.so
LoadModule authz_core_module /usr/lib/apache2/modules/mod_authz_core.so
and change the Proxy
authorization section to be:
Require ip 127.0.0.1
rather than "Order Deny, Allow
", "Deny from all
", "Allow from 127.0.0.1
".
It is now also possible to use additional proxy directives including:
ProxyAddHeaders On
ProxySourceAddress A.B.C.D
but these are optional. With those changes the same minimal configuration approach should work with Apache 2.4.
(Full Apache 2.4 example config.)