Java on Port 443

I’ve been working on setting up a Java based SAML provider. This means that the application needs to handle request and response over HTTPS. And, since often this is deployed in data centers where non-standard ports are blocked, it means that the HTTPS really needs to be supported on the proper port, which is 443. Here are the range of options.

Lets assume the app is being served by Tomcat, although this goes for any HTTP server, especially the interpreter based ones.

You have two choices.

  1. Run Tomcat to listen and serve on port 443 directly
  2. run a proxy in front of it.

For proxies, you have three easy choices, and many others, if you are running on Fedora/RHEL/Centos.

  1. IP tables. Listen on port 443, forward to the local port that Tomcat is listening on for HTTPS
  2. Apache  forwarding either HTTP or AJP
  3. HA Proxy forwarding HTTP

Those each have configuration specific issues.  I am not going to go deep in to them here.

Lets return to the case where you want Tomcat to be able to directly listen and respond on port 443.

Your first, and worst, option is to run as root.  Only root is able to listen to ports under 1000 on a default Linux setup.

Apache (and the others) does something like this.  But it uses Unix specific mechanisms to drop privileges.   So when you run PS, you can see that the HTTPD process is running as apache or nobody or httpd depending on your distro.  Basically, to process runs as root, listens on port 443, and then tells the Kernel to downgrade its userid to the less priviledged one.  It might change groups too, depending on how its coded.

Java could, potentially do this, but it would take a JNI call to make the appropriate System call.  Tomcat can’t really handle that.  It also prevents you from re-opening a closed connection.  While Apache tends to fork a new process to handle that problem, Tomcat is not engineered that way.  You might be coding yourself into a corner.

It turns out that the application does not need access to everything that roots does.  And this is a pattern that is not restricted to network listeners. Thus, a few Kernel versions ago, they added “capabilites” to the Kernel. This seems like a better solution. Specifically, our application needs

CAP_NET_BIND_SERVICE
Bind a socket to Internet domain privileged ports
(port numbers less than 1024).

Can we add this to a Tomcat app?

Lets do a little test.  Instead of Tomcat, we can use something simpler:  The EchoServer code used a Princeton Computer Science Class.  Download EchoServer.java, In.java and Out.java.

Compile using

javac EchoServer.

And run using

java EchoServer 4444

In another window, you can telnet into the server and type in a struing, which will be echoed back to you.

$ telnet localhost 4444
Trying ::1...
Connected to localhost.
Escape character is '^]'.
test
test

If you Ctrl C the echo server, you will close the connection.

Ok, what happens if we try this on a port under 1000?  Lets try.  First, edit EchoServer.java so it is listening on port 400, not 4444.

$ diff -u EchoServer.java.orig EchoServer.java
--- EchoServer.java.orig 2018-02-17 18:09:42.846674768 -0500
+++ EchoServer.java 2018-02-17 18:09:57.211684501 -0500
@@ -26,7 +26,7 @@
 public static void main(String[] args) throws Exception {
 
 // create socket
- int port = 4444;
+ int port = 400;
 ServerSocket serverSocket = new ServerSocket(port);
 System.err.println("Started server on port " + port);

Recompile and run:

$ java EchoServer 400
Exception in thread "main" java.net.BindException: Permission denied (Bind failed)
 at java.net.PlainSocketImpl.socketBind(Native Method)
 at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387)
 at java.net.ServerSocket.bind(ServerSocket.java:375)
 at java.net.ServerSocket.<init>(ServerSocket.java:237)
 at java.net.ServerSocket.<init>(ServerSocket.java:128)
 at EchoServer.main(EchoServer.java:30)

How can we add CAP_NET_BIND_SERVICE?  We have the setcap utility:

setcap – set file capabilities

However…it turns out that this is set on an executable?  What executable?  Can’t be a shell script, as the capabilites are dropped wen the shell executes the embeedded interpreter.  We would have to set it on the Java executable itself.  This is, obviously, a dangers approach, as it means ANY Java program can listen on any port under 1000, but lets see if it works.

First we need to find the Java executable:

[ayoung@ayoung541 echo]$ which java
/usr/bin/java
[ayoung@ayoung541 echo]$ ls -a\l `which java`
lrwxrwxrwx. 1 root root 22 Feb 15 09:10 /usr/bin/java -> /etc/alternatives/java
[ayoung@ayoung541 echo]$ ls -al /etc/alternatives/java
lrwxrwxrwx. 1 root root 72 Feb 15 09:10 /etc/alternatives/java -> /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-5.b14.fc27.x86_64/jre/bin/java

Can we use this directly?  Lets see:

$ /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-5.b14.fc27.x86_64/jre/bin/java EchoServer 
Exception in thread "main" java.net.BindException: Permission denied (Bind failed)
 at java.net.PlainSocketImpl.socketBind(Native Method)
 at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387)
 at java.net.ServerSocket.bind(ServerSocket.java:375)
 at java.net.ServerSocket.<init>(ServerSocket.java:237)
 at java.net.ServerSocket.<init>(ServerSocket.java:128)
 at EchoServer.main(EchoServer.java:30)

Looks OK.  Lets try setting the capability:

[ayoung@ayoung541 echo]$ /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-5.b14.fc27.x86_64/jre/bin/java EchoServer 
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-5.b14.fc27.x86_64/jre/bin/java: error while loading shared libraries: libjli.so: cannot open shared object file: No such file or directory
[ayoung@ayoung541 echo]$ java EchoServer 
java: error while loading shared libraries: libjli.so: cannot open shared object file: No such file or directory

Something does not like that capability.  We an unset it and get the same result as before.

[ayoung@ayoung541 echo]$ sudo /sbin/setcap cap_net_bind_service=-ep /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-5.b14.fc27.x86_64/jre/bin/java
[ayoung@ayoung541 echo]$ java EchoServer 
Exception in thread "main" java.net.BindException: Permission denied (Bind failed)
 at java.net.PlainSocketImpl.socketBind(Native Method)
 at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387)
 at java.net.ServerSocket.bind(ServerSocket.java:375)
 at java.net.ServerSocket.<init>(ServerSocket.java:237)
 at java.net.ServerSocket.<init>(ServerSocket.java:128)
 at EchoServer.main(EchoServer.java:30)

Seems I am not the first person to hit this, and a step by step is laid out in the answer here.

Once I add exactly the path:

 $ cat /etc/ld.so.conf.d/java.conf
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-5.b14.fc27.x86_64/jre/bin/../lib/amd64/jli

And run

[ayoung@ayoung541 echo]$ java EchoServer 
Started server on port 400

OK, that works.

Would you want to do that?  Probably not.  If you did, you would probably want a special, limited JDK availalbe only to the application.

It is also possible to build a binary that kicks off the Java process, add the capability to that, and further limit what could call this code.  There is still the risk of someone running it with a different JAVA_PATH and getting different code in place, and using that for a privilege elevation. The only secure path would be to have a custom classloader that read the Java code from a segment of the file; static linking of a Jar file, if you will. And that might be too much. However, all these attacks are still possible with the Java code the way it is set up now, just that we would expect a system administrator to lock down what code could be run after configuring, say, an HTTPD instance as a reverse proxy.

3 thoughts on “Java on Port 443

  1. Not sure if this works, but have you looked into using systemd and its ambient capabilities directive for this?

    Something along these lines:
    1. Wrap `java EchoServer` into shell-script
    2. systemd-run –system -p AmbientCapabilities=net_bind_service -p User=joe /usr/bin/shell-script

  2. I have not. Thanks for pointing out. I think that might be an extension of the general rules on capabilities, which would make sense. I think that means you still need the of config registration for the library paths.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.