This workshop is a practical guide to securing a web service in the de.NBI Berlin OpenStack environment. You'll learn how to expose a service to the internet safely, using a reverse proxy, load balancer, and firewall rules, complemented by SSL/TLS encryption and authentication.
- Network Setup: Configure a secure network architecture using the OpenStack graphical user interface.
- Virtual Machines & Docker: Create and set up virtual machines to host a simplified, containerized web service.
- Reverse Proxy: Implement a Dockerized reverse proxy to manage HTTP/S encryption and authentication.
- Automated HTTPS: Discover the key mechanisms to achieve automated SSL/TLS certificates for your service.
This workshop emphasizes hands-on application and best practices for securing your own cloud-based web services. We could dive into more detailed topics and questions as time permits.
- Load Balancer: A device or service that efficiently distributes incoming network traffic across a group of backend servers. Its purpose is to increase the capacity, reliability, and availability of an application.
- Reverse Proxy: A server that sits in front of one or more web servers, forwarding client requests to those servers. It acts as a single point of entry, providing an extra layer of security, and can handle tasks like SSL/TLS encryption and caching.
- SSL/TLS: Stands for Secure Sockets Layer / Transport Layer Security. These are cryptographic protocols that provide secure communication over a computer network. They are essential for encrypting data sent between a user's browser and a web server, protecting it from eavesdropping and tampering.
- HTTPS: Stands for Hypertext Transfer Protocol Secure. It is the secure version of HTTP, the protocol used to send data between a web browser and a website. The 'S' at the end of HTTPS stands for 'Secure', meaning all communications between your browser and the website are encrypted via SSL/TLS.
- Authentication: The process of verifying the identity of a user, service, or device. It ensures that only authorized parties can access a system or resource. We'll explore methods like BasicAuth and maybe O2AUTH to control access to our web service.
| Tool/Technology | Description |
|---|---|
| OpenStack | A suite of open-source software for creating and managing private and public clouds. In this workshop, we'll use it to provision our virtual machines, networks, and security rules. |
| OpenStack Octavia | The native load-balancing-as-a-service component of OpenStack. We'll use it to distribute incoming traffic and route it to our reverse proxy. |
| Docker | A platform for developing, shipping, and running applications in containers. We'll use it to package our web service and reverse proxy, ensuring they are portable and easy to manage. |
| Python HTTP Server | A simple web server written in Python. This will be our "dummy" web service to demonstrate the security principles. |
| Caddy | An open-source web server with powerful reverse proxy capabilities. We'll use it for its built-in automation of HTTPS certificate provisioning via Let's Encrypt. Caddy simplifies SSL/TLS encryption, making it easy to secure web traffic. |
- Everyone has access to the OpenStack project "CLUM2025SecWeb1".
- Everyone has added a Public SSH Key to the OpenStack environment for remote access.
Before deploying our VMs, we will create a network infrastructure suitable for a secure setup. This involves two key networks and their associated security groups.
CLUM2025SecWeb-dmz-int-network: An internal network with a subnet that connects to the dmz-routerCLUM2025SecWeb-dmz-routerand the external floating IP pooldmz, allowing our Octavia Load Balancer to receive internet traffic and redirect internally to the reverse-proxy VM.CLUM2025SecWeb1-network-2: An internal network and subnet that connects to the public-routerCLUM2025SecWeb1-router-2and the external floating IP poolpublic, allowing our VM's to access the internet and be accessable via the User-Jumphost for remote-access. This network is isolated from direct public access networkdmz.CLUM2025SecWeb1-reverseproxy-network: An internal network and subnet, dedicated to the reverse-proxy VM that connects to the public-routerCLUM2025SecWeb1-router-2and the external floating IP poolpublic, allowing our VM's to access the internet and be accessable via the User-Jumphost for remote-access. This network is isolated from direct public access networkdmz.
We will configure three separate Security Groups to act as our firewalls(locally on the VM):
ReverseProxy-SecGroup-<YOUR_NAME>: This group handles inbound traffic from the load balancer to the reverse proxy. It will be configured to allow ingress on ports80 (HTTP)and443 (HTTPS). The connection is ment to be used from the internet so allow all incoming traffic with 0.0.0.0/0.Webservice-SecGroup-<YOUR_NAME>: This group controls traffic from the reverse proxy to the internal web service. For example, if our webservice listens on port8080, this group will allow ingress on that port. Granularity is recommended (e.G. just allow the reverse-proxy to access the webservice VM)SSHJumphost-SecGroup-<YOUR_NAME>: This group controls the ssh connection fromdenbi-jumphost-01.bihealth.orgto the VMs over port22 (SSH). Granularity is recommended (e.G. just allow the jumphost to access the VMs)
Now we will launch two virtual machines (VMs) and connect them to our private network and corresponding security groups.
- Name:
reverse-proxy-<YOUR_NAME> - Network:
CLUM2025SecWeb1-reverseproxy-network - Security Group:
ReverseProxy-SecGroup-<YOUR_NAME>,default - Flavor:
de.NBI default - Image:
Ubuntu-24.04-Docker - Key Pair: Select the SSH key pair you added as a prerequisite
- Name:
web-service-<YOUR_NAME> - Network:
CLUM2025SecWeb1-network-2 - Security Group: ``Webservice-SecGroup-<YOUR_NAME>
,default` - Flavor:
de.NBI default - Image:
Ubuntu-24.04-Docker - Key Pair: Select the SSH key pair you added as a prerequisite.
After launching the instances, you can associate a floating-ip from the pool public to your VMs to have remote-access.
The load balancer is our entry point from the internet. It will distribute traffic to our reverse proxy VM.
-
Create a Load Balancer: In the OpenStack dashboard, navigate to Network > Load Balancers and click Create Load Balancer.
- Name:
workshop-lb-<YOUR_NAME> - Subnet: Select the
CLUM2025SecWeb-dmz-int-networksubnet. This is crucial as it connects the load balancer to the public network.
- Name:
-
Add a Listener: A listener defines the protocol and port on which the load balancer listens for incoming traffic.
- The first listener is created when the loadbalancer is created, for additional listeners, select the
workshop-lbload balancer and go to the Listeners tab. Click Add Listener. - Name:
http-listener - Protocol:
TCP - Port:
80 - Default Pool: Create a new pool called
http-pool.
- The first listener is created when the loadbalancer is created, for additional listeners, select the
-
Configure the Pool: A pool is a group of backend servers (in our case, the reverse proxy VM) that will handle the traffic.
- After creating the listener, you'll be prompted to configure the pool.
- Protocol:
TCP - Load Balancing Method:
ROUND_ROBIN - Health Monitor: Create a
HTTPhealth monitor to check if the reverse proxy is up and running.- Type:
TCP - Delay:
5(seconds) - Timeout:
3(seconds) - Max Retries:
3
- Type:
-
Add Members to the Pool: Finally, add your
reverse-proxyVM as a member of the pool.- Navigate to the
http-pooland click Add Member. - IP Address: Enter the internal IP address of your
reverse-proxyVM. - Port:
80(The reverse proxy will be configured to listen on this port).
- Navigate to the
-
Repeat for HTTPS: Follow the same steps to create a second listener for HTTPS traffic.
- Listener Name:
https-listener - Protocol:
TCP - Port:
443 - Certificate: You will need to upload a security certificate for this.
- Default Pool: Create a new pool called
https-pool. Add thereverse-proxyVM as a member with port443.
- Listener Name:
-
Load Balancer Floating IP: Add the public IP from the
dmz-extnetwork to your loadbalancer to make it accessable from the internet.- Go to the
Load Balancersoverview and click onAccociate Floating IPon the right side of your loadbalancer. - Floating IP: Attach an existing and predefined floating-ip from the pool
dmzto your load balancer. This will be the public IP address of your web service and is connected to the dns-entry.
- Go to the
Now, your load balancer is configured to receive internet traffic on its floating IP and forward it to your reverse proxy VM, providing a secure and scalable entry point for your web service.
We will now configure our VMs to run the web service and the reverse proxy using Docker.
-
Clone the GitHub Repository:
- SSH into both the
web-serviceandreverse-proxyVMs. - On each VM, clone the workshop repository containing the Docker files:
git clone https://github.com/deNBI/securing_openstack_clum2025.git cd securing_openstack_clum2025
Repository tree view
βββ Docker βΒ Β ββ proxy βΒ Β βΒ Β βββ caddy_basicauth βΒ Β βΒ Β βΒ Β βββ Caddyfile βΒ Β βΒ Β βΒ Β βββ Dockerfile βΒ Β βΒ Β βΒ Β βββ docker-compose.yml βΒ Β βΒ Β βββ caddy_oauth2 βΒ Β βΒ Β βββ Caddyfile βΒ Β βΒ Β βββ Dockerfile βΒ Β βΒ Β βββ docker-compose.yml βΒ Β βββ web-app βΒ Β βββ fast-api βΒ Β βΒ Β βββ Dockerfile βΒ Β βΒ Β βββ app βΒ Β βΒ Β βΒ Β βββ __init__.py βΒ Β βΒ Β βΒ Β βββ main.py βΒ Β βΒ Β βββ docker-compose.yml βΒ Β βΒ Β βββ requirements.txt βΒ Β βββ python-webserver βΒ Β βββ Dockerfile βΒ Β βββ docker-compose.yml βΒ Β βββ server.py βββ README.md - SSH into both the
-
Deploy the Web Service:
-
On the
web-serviceVM, navigate to the directory./Docker/web-app/python-webserverwith thedocker-compose.ymlfile for the web service. -
Check the exposed ports in the
docker-compose.ymlandserver.py. The preconfigured port is8080. You could adapt it if you want to but dont forget you need to adapt the reverse-proxy aswell as the SecGroupsdocker-compose.yml
services: web_server: build: . container_name: python_web_server ports: - "8080:8080"server.py
from http.server import HTTPServer, BaseHTTPRequestHandler import socketserver class S(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(b'Welcome to CLUM 2025 Secure a Webservice') if __name__ == "__main__": PORT = 8080 Handler = S httpd = socketserver.TCPServer(("", PORT), Handler) print("serving at port", PORT) httpd.serve_forever() -
Start the web service container:
sudo docker compose up -d
-
Test the connection: From the
reverse-proxyVM, usecurlto verify that you can reach the web service on its internal IP address.curl http://<web-service-internal-ip>:8080
You should receive a response from the web service.
-
Note
If you need to make changes to the containers, you need to rebuild the images for the changes to be deployed. After changing the files restart the containers with sudo docker compose up --force-recreate --build -d
- Deploy the Reverse Proxy:
-
On the
reverse-proxyVM, navigate to the directory./Docker/proxy/caddy_basicauthwith thedocker-compose.ymlfile for the reverse-proxy. -
Check the docker-compose.yml
services: caddy_reverse_proxy_basicauth: build: . network_mode: "host" container_name: caddy_reverse_proxy_basicauth volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data volumes: caddy_data: -
Adapt the Caddyfile:
# With the DNS-Name provided Caddy automatically tries to provide a certification via LetsEncrypt, so make sure your Loadbalancer has the same FloatingIP then the corresponding DNS-Entry you are using in the following config <YOUR_DNS_ENTRY> { reverse_proxy <WEBSERVER_IP:WEBSERVER_PORT> basic_auth { admin $2a$14$7JTsZSWdHLIe.GHyPdpImu0iQrk6HpwUFQ5iRl895zp6x/kxcELIC # Created account: user: admin pw: sicher # For adding new user you need to provide <USERNAME> <HASHED_PASSWORD> # sudo docker run --rm caddy caddy hash-password --plaintext <PASSWORD_2_HASH> } }- Adapt the placeholder for the
<YOUR_DNS_ENTRY>with one of the dns-entries we provide for you. We already defined the dns-entries with corresponding floating-ips from thedmzpool - Adapt the placeholder for the
reverse_proxy <WEBSERVER_IP:WEBSERVER_PORT>with the internal IP address and the exposed port of yourweb-serviceVM. - If you want to add a new user to basic_auth you have to define it in the Caddyfile with a username and a hashed password
- Adapt the placeholder for the
-
Start the reverse proxy container:
sudo docker compose up -d
-
Check the Caddy logs: View the logs to confirm that Caddy has successfully obtained an SSL/TLS certificate from Let's Encrypt.
sudo docker logs caddy-container-name
Look for messages indicating successful certificate acquisition.
-
Test the connection: From the
web-serviceVM, usecurlto test the reverse proxy. This only works if the Caddyfile has also an entry for the internal IP of the reverse proxy without authentication.curl https://<reverse-proxy-internal-ip>
-
You should now be able to access the webservice via the dns-hostname of your setup via the internet.
If the service is not availabel over the internet, you can use several tools to make sure the setup works as expected and to find errors or issues.
To find out what the container is doing and to see possible error messages, you should look at the log files.
- Take a look into the running container logs:
sudo docker logs <container-name> -fThis will show the running log for the container and add messages as they are written. - If you can not find an error or need more information you can take a look in the logfiles inside of the container itself. To do this open a console in the container and find the internal log files:
sudo docker exec -it <container-name> /bin/shDepending on the image used to build the container, the installed shell might differ, or there might even be no shell installed, at all. Log files are often stored in/var/log.
The containers are communicating over ports with the vm and the vm is communication with other vms and the loadbalancer over ports. Security groups are used to open specific ports or block networks or IP addresses.
- Take a look at the security groups and the allowed ip addresses and ports.
- To see the ports used by the containers use:
sudo docker port <container-name>If this command does not show any used ports your container is either using the host network (look in thedocker_compose.ymlfor "host" in the sectionnetwork_mode) or is not configured the way you want. - To see the ports the vm listens on use:
ss -tulpan - For a detailed record of all traffic you can use tcpdump, but use this with caution. Some networks should not be listed on and the output might be to much to use. This can be seen as intrusion if performed on secured networks that are monitored. So only use this if you are sure it is secure and necessary. Look for the interface you want to observe with
ip aand add a port to see only traffic for this port.sudo tcpdump -i enp3s0 port 443