Setting up HTTPS for containerized Java services

Nowadays, an increasing number of Internet resources and applications are declaring a complete transition to a data transfer protocol using HTTPS encryption. Moreover, some of them are tightening their encryption requirements. Now, if you, for example, try to open a resource on which a self-signed certificate was installed via an encrypted channel in the browser, you may not only receive a warning about an unsecure connection, but also stop the connection attempt. All these changes are fraught with various kinds of inconvenience for both specialists and end users.

Our colleague Alexey Oblozhko will talk about the practical side of using certificates. We give him the floor.

Let's create a simple web application in Java and make it ready for use as a containerized application running over the HTTPS protocol. To create the application we will use the framework Jmixwhich is based on Spring Boot and Vaadin, so the described approaches will also work for a wide class of Spring Boot web applications.

We assume that you have installed the latest version of Docker for your OS using brew, chocolately or deb/rpm.

Creating an application

To develop on Jmix, you need to install JDK 17 or 21, as well as IntelliJ IDEA (the Community edition is sufficient, which can be downloaded from the website https://www.jetbrains.com/idea/download/). After launching the IDE, install the Jmix plugin from the JetBrains marketplace.

To create a new project in the Jmix framework, you can use the IntelliJ IDEA new project wizard by selecting the appropriate project type there. But we will take a jmix-onboarding project specially prepared for training, which already has a small model, UI, and test data.

To get it, select File -> New -> Project from Version Control and in the dialog that appears, indicate the repository address: https://github.com/jmix-framework/jmix-onboarding-2.git.

After the development environment has downloaded, assembled and indexed everything, we can immediately launch the application by selecting the Jmix Application configuration in the top panel of the IDE.

As soon as lines about Server startup appear in the logs, you can open the address in the browser http://localhost:8080 and log in to the login form with the username and password admin.

Our task today is to make it so that you can enter https in the address bar and get the same application, but working according to current data transfer standards.

Certificates for development

For development, you can generate self-signed certificates (for example, as described in our documentation). However, they no longer always work in browsers and other applications that use web protocols. A solution for the developer would be to issue their own root certificate and sign the server certificate with it.

To simplify the task, you can use the mkcert utility. Installation instructions are provided in the project help, which lives at: https://github.com/FiloSottile/mkcert.

As a result of the installation, your Path should contain the mkcert executable file. By calling it from a terminal emulator, we can generate and install a root certificate in an easier way than if we did it with openssl and an understanding of the OS features regarding working with security certificates. Let's generate and install a root certificate:

mkcert -install

Now let's go to the project resources and generate a server certificate there:

mkcert -pkcs12 localhost 127.0.0.1 ::1 

The password will be set to changeit.

Let's create a certificate store with the generated certificate:

keytool -importkeystore -srckeystore localhost+2.p12 -srcstoretype pkcs12 -destkeystore localhost.jks 

Let's enter something simple as a password, for example the same changeit. It will need to be specified in the configuration below.

Let's add the configuration for the application to application.properties:

# Enables HTTPS 
server.ssl.enabled=true 
# The format used for the keystore 
server.ssl.key-store-type = JKS 
# The path to the keystore containing the certificate 
server.ssl.key-store = classpath:localhost+2.jks 
# The password used to generate the keystore 
server.ssl.key-store-password = changeit 
# The alias mapped to the certificate 
server.ssl.key-alias = 1 
# Changes the server's port 
server.port = 8443 

Now you can launch the application (or restart it if you have already done so). When a link to https://localhost:8443we can open it in the browser and make sure that we are not shown any warnings about an insecure connection.

Packaging an application in containers

The out-of-the-box project uses local HSQL as a database. This means that every time the container is restarted, the database will be recreated. Thanks to integrated Liquibase migrations, the schema will be generated automatically. When moving to a production environment, it will not be difficult for you to change the type of data source in the project configuration to use a full-fledged DBMS by adding appropriate containers to the Docker architecture.

As for HTTPS, we will need to comment out the entire section that we added earlier, since encryption in the new realities will be taken over by the front-end proxy server.

The container image is collected with the command:

./gradlew -Pvaadin.productionMode=true bootBuildImage 

Let's get productive

For productive servers, you will need to either purchase SSL certificates and replace the generated ones with them, or set up free generation of less prestigious certificates using acme.sh scripts (https://github.com/acmesh-official/acme.sh) or certbot from letsencrypt. The first option is most often used as an addition to containerized proxy servers, with the help of which they independently obtain and update certificates; the second is usually performed manually or according to an OS schedule.

To issue certificates, in general, you need to have a domain name and some stable server IP address. We will further refer to the domain name as mydomain.ru.

Setting up domain records

In the DNS settings, you need to add A records @ and * pointing to the server ip. You can check their operation with the commands:

nslookup mydomain.ru

Setting up an HTTPS proxy

Install the acme.sh script:

wget -O - https://get.acme.sh | sh -s email=mymail@example.com

Some registrars may provide their own version of the script. Information can be found in the sections related to DNS configurations of the registrar admin panel.

We install Docker on the server and run nginx-proxy with the acme.sh integration add-on. Many experts find it more convenient to use the docker compose yml format to describe launched services, but we decided to leave the option with command line arguments for examples.

docker run --detach \ 
    --name nginx-proxy \ 
    --publish 80:80 \ 
    --publish 443:443 \ 
    --volume certs:/etc/nginx/certs \ 
    --volume /etc/nginx/vhost.d:/etc/nginx/vhost.d \ 
    --volume html:/usr/share/nginx/html \ 
    --volume /var/run/docker.sock:/tmp/docker.sock:ro \ 
    nginxproxy/nginx-proxy 
docker run --detach \ 
    --name nginx-proxy-acme \ 
    --volumes-from nginx-proxy \ 
    --volume /var/run/docker.sock:/var/run/docker.sock:ro \ 
    --volume acme:/opt/letsencrypt_acme/acme.sh \ 
    --env "DEFAULT_EMAIL=mymail@example.com" \ 
    nginxproxy/acme-companion 

The command above specifies the e-mail that will be used to receive automatic notifications.

–volume acme:.. should point to a previously installed script.

Deploying the docker image registry

Instead of using your own image registry, you can use DockerHub and skip this step, using its credentials for authentication. Using your own registry, however, can have its advantages: traffic on the local network will flow faster, which will have a beneficial effect on the speed of your builds.

Now, in order for our server to receive the docker images we developed, we need to deploy its own registry.

In the user's home directory, create a directory with a docker project:

mkdir docker-registry 
cd docker-registry 
mkdir auth 

To generate a password, you will need to install the apache2-utils package on the system. This can be done with a native package manager if you're on Linux, brew on MacOS, and chocolately on Windows. As a result, the following command should start working:

htpasswd -bnB user password > auth/htpasswd 

Now we are ready to launch the docker image registry service. In the command, we will immediately define the parameters for nginx-proxy: VIRTUAL_PORT will indicate which port to proxy, and VIRTUAL_HOST and LETSENCRYPT_HOST will indicate which subdomain to use. Instead of registry.mydomain.ru you need to specify the subdomain of your domain.

docker container run -d -p 5000:5000 --name registry -v "$(pwd)"/auth:/auth -e REGISTRY_AUTH=htpasswd -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd -e VIRTUAL_HOST=registry.mydomain.ru -e VIRTUAL_PORT=5000 -e LETSENCRYPT_HOST=registry.mydomain.ru registry 

With such an architecture, it would be reasonable to close all external ports except the standard ones for the web 80 and 443. However, this is usually always done.

Let's also increase the request size so that our docker images can get through the proxy server restrictions:

{ echo 'client_max_body_size 5000m;'; } > /etc/nginx/vhost.d/registry.mydomain.ru 

After changing the configuration, the service must be restarted:

docker restart registry

Setting up publication of the project image

Once the registry is deployed, we can build and publish the image from the local computer.

You need to add the following lines to the docker image publishing configuration for the project, namely the build.gradle file:

bootBuildImage { 
    imageName = "registry.mydomain.ru/jmixonboarding:0.0.1-SNAPSHOT" 
    publish = true 
    docker { 
        publishRegistry { 
            url = "https://registry.mydomain.ru/" 
            username = "user" 
            password = "password" 
        } 
    } 
} 

After this, you can build and publish the docker image:

./gradlew bootBuildImage -Pvaadin.productionMode=true 

If we build and publish a new version on clients, we must not forget to do a docker pull.

On the server, log in to the registry:

docker login -u username –p password https://registry.mydomain.ru 

If we build and publish a new version on clients, we must not forget to do a docker pull.

On the server, log in to the registry:

docker login -u username –p password https://registry.mydomain.ru

and run the application image:

docker run --detach \ 
    --name testnginx \ 
    --env "VIRTUAL_HOST= jmixonboarding.mydomain.ru" \ 
    --env "VIRTUAL_PORT=8080" \ 
    --env "LETSENCRYPT_HOST= jmixonboarding.mydomain.ru" registry.mydomain.ru:5000/ jmixonboarding:0.0.1-SNAPSHOT 

Now, so that all our containers start automatically, let’s run the command to add a restart policy for each:

docker update --restart unless-stopped nginx-proxy 
docker update --restart unless-stopped nginx-proxy-acme 
docker update --restart unless-stopped testnginx

Thus, we were able to launch a minimal application on Jmix, running over HTTPS both locally at the developer’s site and on the production server.

Similar Posts

Leave a Reply

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