Running a servlet container without Apache that needs to bind to ports < 1024 but still run as a non-root user usually requires special setup (unless using Debian or Ubuntu). Some containers include tools to assist with this, the Linux kernel also has features that can help enable this, or another option is to rely on port mapping.
In this approach the Java servlet container listens on a high port and the local packet filter is used to forward requests from standard ports (80, 443) to those high ports.
One caution regarding this approach is that it will cause your IdP to fail if the port mapping software is stopped. Normally dropping a firewall doesn't prevent existing services from running, but this approach changes that situation. You should take care that any administrative staff are well aware of this change.
One way to deal with this issue is generating the iptables (or firewalld) rules dynamically on each start of the container, which is easy with systemd. See also below (POSIX capabilities) for other examples using this technique.
For non-Red Hat Linux installations modify /etc/rc.d/rc.local to include the following lines:
/sbin/iptables -t nat -A PREROUTING -p tcp -m tcp --dport 80 -j DNAT --to-destination 10.0.0.10:8080 /sbin/iptables -t nat -A PREROUTING -p tcp -m tcp --dport 443 -j DNAT --to-destination 10.0.0.10:8443 |
For Red Hat Linux installations using iptables (Red Hat 6 and earlier by default) modify the nat section of the /etc/sysconfig/iptables to include the following lines:
*nat :PREROUTING ACCEPT [4332:289016] :POSTROUTING ACCEPT [43:2689] :OUTPUT ACCEPT [43:2689] -A PREROUTING -p tcp -m tcp --dport 80 -j DNAT --to-destination 10.0.0.10:8080 -A PREROUTING -p tcp -m tcp --dport 443 -j DNAT --to-destination 10.0.0.10:8443 COMMIT |
Note the changes are only the addition of the DNAT lines in the nat section.
For Red Hat Linux installations using firewalld (Red Hat 7 and later by default, unless you specifically switched back to iptables), issue the following commands as root:
firewall-cmd --zone=public --add-forward-port=port=80:proto=tcp:toport=8080 --permanent firewall-cmd --zone=public --add-forward-port=port=443:proto=tcp:toport=8443 --permanent firewall-cmd --zone=public --add-port=80/tcp --permanent firewall-cmd --zone=public --add-port=443/tcp --permanent firewall-cmd --reload |
Debian, Ubuntu and derivatives come with the authbind utility and integrate this with their Tomcat packages by default (only needs to be enabled). Others can download and build the software from source code (or try an inofficial RPM spec file), of course, but that also requires changed Tomcat startup scripts. Latest changes to authbind were made in 2012 (as per Aug. 2016), so it's not a particularly fast-moving target (i.e., no need to frequently update and rebuild the software, it's very stable).
Check the IdPLinuxNonRootDebianUbuntu page for configuration details.
On systems lacking authbind (e.g. RedHat, CentOS & friends) Apache Commons daemon (jsvc) can be used to manage Tomcat. Instead of the complex layering of shell scripts commonly used in Tomcat distributions (and GNU/Linux distributions of packaged Tomcat), using systemd one can override the packaged service file to use jscv instead (or alternatively create another service/unit file and start that instead of tomcat
). JSVC starts as root – and hence can open ports < 1024 – but runs Tomcat and the JVM as unpriviledged user (here set to "tomcat" in the jsvc command below).
apache-commons-daemon-jsvc
packageinit
s left as excercise to the reader)The examples below assume a CentOS 7 system and use of the packaged Tomcat 7 (no Tomcat 8 available for CentOS 7 ) and OpenJDK 8 software. Configuration parameters can be read from a file (here CentOS's default config file /etc/tomcat/tomcat.conf
is used, but you can also supply your own, with arbitrary NAME=value assignments) and so can be tuned independently from the systemd service file.
The following unit file tries to mirror the default Tomcat startup scripts from CentOS 7's tomcat package as closely as possible, only using
Note that with
We therefore changed the
|
To address the problem of unpriviledged users not being able to bind to priviledged ports head-on POSIX capabilities can be used to allow just that. The java binary used to run the servlet container needs to be given this capability.
incron
, to ensure changes are re-applied after each Java upgrade)The examples below assume a CentOS 7 system and use of the packaged Tomcat 7 (no Tomcat 8 available for CentOS 7 ) and OpenJDK 8 software. First try everything manually on the command line (possibly adjusting the paths, this is on AMD64 using all defaults):
yum install tomcat java-1.8.0-openjdk-headless libcap && systemctl enable tomcat setcap cap_net_bind_service=+ep /usr/lib/jvm/jre/bin/java echo /usr/lib/jvm/jre/lib/amd64/jli/ > /etc/ld.so.conf.d/java.conf ldconfig |
Then set your HTTP connector to port="443" or "80" in /etc/tomcat/server.xml and restart Tomcat. Check the status output, listening processes and the process list to make sure it's the unpriviledged tomcat
user that's running Java here.
systemctl restart tomcat systemctl status tomcat netstat -ltnp # what process listens where ps auxww # what user does that process run as |
Only continue with automating/productionalizing this approach once you got this working.
The |
Copy the content of the systemd unit file (included below, expand to view) into a new file /etc/systemd/system/tomcat.service
or alternatively run
systemctl edit --full tomcat |
which spawns an editor with the current unit file and add the lines between BEGIN
and END
to the existing unit file, in the same place (after EnvironmentFile and before ExecStart). JAVA_HOME
should come from one of the referenced EnvironmentFile
s, but e.g. if your system is not of the "amd64" arch you may need to change the paths, both for the java
binary as well as the one to lib/amd64/jli/libjli.so
.
|
Save, exit the editor and run:
systemctl daemon-reload systemctl restart tomcat systemctl status tomcat |
and verify that everything is still working (Java listening on a low port, java
process being run as Tomcat).
While Tomcat is still running verify that the capabilities are there on the Java binary:
# getcap /usr/lib/jvm/jre/bin/java /usr/lib/jvm/jre/bin/java = cap_net_bind_service+ep |
After you stop Tomcat again they should be gone (to be added dynamically again on each start):
systemctl stop tomcat getcap /usr/lib/jvm/jre/bin/java |
Running a seperate server process only to proxy all requests to the servlet container is not ideal. At the very least doing that means that two servers/services must be correctly configured, integrated and running in order for one web server/service to function (i.e., doubling the potential for service disruptions). None of the approaches introduced above suffer from these issues. Additionally, proxying may introduce performance or timeout problems, problems with proper virtualization of requests, getting the correct IP addresses logged for clients, security issues with HTTP Request Headers set by an external web server / load balancer (which may be forged), etc.
The example given here assumes use of AJP between Tomcat and httpd. TLS is offloaded to httpd. Tomcat only listens on port 8009 on the loopback interface (inaccessible to the outside world), httpd listens on port 443 for TLS (and maybe also 8443 for backchannel support).
In Tomcat's server.xml only configure an AJP Connector on the loopback interface and disable all other Connectors:
<Connector port="8009" address="127.0.0.1" enableLookups="false" redirectPort="443" protocol="AJP/1.3" maxPostSize="100000" /> |
In Apache httpd
with mod_ssl
configure a VirtualHost
with TLS (not covered here) and proxy requests to the idp
context to the servlet container (adjust to taste if your IDP runs in a different context):
<Proxy ajp://127.0.0.1:8009/idp/*> Require all granted </Proxy> ProxyPass /idp/ ajp://127.0.0.1:8009/idp/ |
If you offer support for a backchannel repeat the same on the backchannel 8443 vhost, using the IDP's backchannel key pair, not the webserver's SSL one), making sure httpd does not get in the way with evaluation of client certificates by the IDP application (SSLVerifyClient optional_no_ca):
<Location /idp> Require all granted SSLOptions +StdEnvVars +ExportCertData SSLVerifyClient optional_no_ca SSLVerifyDepth 10 </Location> <Proxy ajp://localhost:8009/idp/*> Require all granted </Proxy> ProxyPass /idp/ ajp://localhost:8009/idp/ |
One example for this would be use of plain HTTP between the servlet container and the web server (e.g. Nginx, Lighttpd, Pound, etc.). TLS is offloaded to/terminated at the web server. The web server listens on port 443 and proxies all requests to the container, which is listening on port e.g. 8000 with a plain HTTP connector. The container must not be directly accessible from the outside world (e.g. by only listening on the loopback interface).
For backchannel support httpd may also listen on port 8443, proxying requests to the same container port (e.g. 8000) – but possibly a different port on the container might be needed (e.g. 8080) to tell the ports appart in the IDP application. Either way, the keypair must differ for the backchannel port (i.e., the IDP's backchannel credentials, not web server TLS keys), as usual.
The container may need additional configuration to correctly virtualize the scheme (https vs. http), port (443 vs. 8000) and possibly host name. E.g. for Apache Tomcat those are the scheme
, proxyPort
and proxyName
attributes on the Connector, respectively.
Most of the considerations as in section "Webservers without AJP support" apply here as well. In addition to those:
Systemd as included in e.g. RHEL/CentOS 7 (from systemd version 209 on) also includes a proxy server that inherits a socket activated by systemd (e.g. port 443, opened with root privileges), connects to a configured server port (e.g. the container listening on port 8443 with a HTTPS connector for web browser TLS use) and bi-directionally forwards all bits.
address="127.0.0.1"
)scheme="https" proxyPort="443"
on a Connector port="9443"
.Examples TBD.