Deploying Nginx in Docker and Configuring SSL Certificate for Local Network

Preface

In my previous post, I successfully set up a LANraragi server and shared it with other devices in my home LAN. However, new issues arose:

  • LANraragi could be accessed without logging in.
  • Even if LANraragi had a login feature, it still used HTTP plaintext transmission, which is equivalent to no password protection.

The root cause of these issues was that devices like smartphones and smart home devices, connected to the LAN, were not trustworthy. They could potentially have backdoors (or ‘users voluntarily transmitting private data to improve service’). Anyway, these issues needed urgent resolution.

The solution is Nginx:

  • Use Nginx’s built-in basic authentication to allow access only with a correct username and password.
  • Utilize Nginx’s reverse proxy feature. Once Nginx is configured with HTTPS, any software being proxied can use the encrypted transmission.

Therefore, the key lies in the configuration of Nginx and SSL certificates. The challenge is that I need to request a certificate for an internal IP address without a corresponding domain name. This article explores these solutions, marking my first encounter with Nginx and SSL certificate configuration.

Nginx Container

Obtain the Default Configuration File

Create a temporary container, copy the nginx.conf from it:

1
2
3
docker run --name tmp-nginx-container -d nginx:1.24
docker cp tmp-nginx-container:/etc/nginx/nginx.conf /opt/nginx.conf
docker rm -f tmp-nginx-container

Docker Compose

1
2
3
4
5
6
7
8
9
10
11
12
13
version: "3"

services:
nginx:
image: nginx:1.24
container_name: nginx
volumes:
- /opt/docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /opt/nginx_ssl:/etc/nginx/ssl/:ro
network_mode: bridge
restart: always
ports:
- 50443:443
  • Besides the configuration file, map a directory to store SSL private key and certificate.
  • Choose the bridge network mode; otherwise, other containers cannot be accessed.
  • Only map the 443 port, while I have no intention to open the 80 port.

SSL Certificate

Create a CA

Create a CA key myCAPK.key and a (self-signed) certificate myCACertificate.crt:

1
openssl req -x509 -sha256 -nodes -days 3650 -newkey rsa:4096 -keyout myCAPK.key -out myCACertificate.crt

The private key and certificate generated by this command can be directly used for Nginx server. However, if another server also needs an SSL certificate, client devices need to manually trust the new certificate, which is troublesome.

If this certificate is used as a CA certificate, as long as devices trust this certificate, all other certificates signed by this CA will be automatically trusted. Therefore, devices only need to manually add the CA certificate only once.

Check Certificate Information

1
openssl x509 -text -noout -in myCACertificate.crt

Trust the CA

As this CA certificate is self-created, the system won’t recognize it, so it needs to be installed.

Without this step, the browser can still establish an HTTPS connection but will prompt that the connection is not secure.

Windows

Control Panel | Internet Options | Content | Certificates

image-20240131172736021

Trust the CA as a root certificate authority.

image-20240131172831043

Android

Different systems have different locations for installing certificates, but the basic options are the same. Here’s an example for Samsung:

Either choose ‘CA certificates’ or ‘VPN and app user certificates’. Do not choose ‘WLAN certificates’; it does not work.

Screenshot_20240201_113254_Settings

IOS

Using iPadOS 15.7 as an example

  1. Open your email, send the CA certificate as an attachment to yourself.

  2. Using the Safari browser, log in to your email, click the attachment, and select “Download Profile” (a prompt will appear: ‘go to settings to confirm installation’).

  3. Go to settings, select “Install Profile,” enter your lock screen password.

  4. Settings | General | About | Certificate Trust Settings: Enable full trust.

    IMG_2032


According to Apple’s new policy,server’s certificate cannot exceed 398 days, otherwise SSL connection errors occur(NET::ERR_CERT_VALIDITY_TOO_LONG.

Create Server Key

1
openssl genrsa -out nginx_server.key 4096

Create Server CSR (Certificate Signing Request)

Create openssl.cnf with the following content:

1
2
3
4
5
6
7
8
9
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
subjectAltName = @alt_names
[alt_names]
# 虚拟机
DNS.1 = CentOS100
IP.1 = 192.168.169.132
# 主机
IP.2 = 192.168.0.105

The configuration file can be more complex, but for this case, these few configurations are sufficient.

  • DNS.x corresponds to hostname/domain.
  • IP.x corresponds to IP addresses.

x is a positive integer.

The only purpose of this file is to bind the server’s domain name and IP with the certificate during CA signing, indicating that the certificate indeed belongs to that server and not someone else pretending. Without this configuration, the browser would show an error: NET::ERR_CERT_COMMON_NAME_INVALID.

Use the following command to create the CSR:

1
openssl req -new -key nginx_server.key -out nginx_server.csr

CA Signing

1
openssl x509 -req -extfile openssl.cnf  -in nginx_server.csr -CA /opt/MyOwnCA/myCACertificate.crt -CAkey /opt/MyOwnCA/myCAPK.key -CAcreateserial -out nginx_server.crt -days 3650

After signing, you will get the private key nginx_server.key and certificate nginx_server.crt ready to be used in Nginx.

Nginx Configuration

SSL

1
2
3
4
5
6
7
8
9
10
11
server {
listen 443 default_server ssl http2;
listen [::]:443 ssl http2;

ssl_certificate /etc/nginx/ssl/nginx_server.crt;
ssl_certificate_key /etc/nginx/ssl/nginx_server.key;

location /... {
proxy_pass ...;
}
}

This is just the basic configuration; there are more SSL directives in practice, but these are sufficient for now.

Basic Authentication

1️⃣First, create a username and password:

1
2
3
4
# Create the file if it doesn't exist
touch /opt/docker/nginx_auth/.htpasswd-lanraragi
# Add user alpha to this configuration file
htpasswd /opt/docker/nginx_auth/.htpasswd-lanraragi alpha

Then, the system will prompt to set a password for the alpha user. To add multiple users, use the same command:

1
htpasswd /opt/docker/nginx_auth/.htpasswd-lanraragi beta

On CentOS, the htpasswd command belongs to httpd-tools. If not installed, the system will prompt to install it.


2️⃣Configure in Nginx:

1
2
3
4
location /api {
auth_basic "Administrator’s Area";
auth_basic_user_file /etc/nginx/.htpasswd;
}
  • auth_basic prompts a message to user on the login page;
  • auth_basic_user_file points to the previously created .htpasswd

It can also be enabled globally and disabled for specific URLs:

1
2
3
4
5
6
7
8
9
server {
...
auth_basic "Administrator’s Area";
auth_basic_user_file conf/.htpasswd;

location /public/ {
auth_basic off;
}
}

Example: Lanraragi

Let’s come back to the initial demand: reverse proxy Lanraragi and provide a login feature.

Docker compose:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
version: "3"

services:
nginx:
image: nginx:1.24
container_name: nginx-lanraragi
volumes:
- /opt/docker/nginx_conf/nginx-lanraragi.conf:/etc/nginx/nginx.conf:ro
- /opt/docker/nginx_ssl:/etc/nginx/ssl/:ro
- /opt/docker/nginx_auth:/etc/nginx/auth/:ro
ports:
- 43000:443
lanraragi:
image: difegue/lanraragi:v.0.9.0
container_name: lanraragi
volumes:
- /mnt/hgfs/doujinshiArchives:/home/koyomi/lanraragi/content:ro
- lanraragi-database:/home/koyomi/lanraragi/database
depends_on:
- nginx

volumes:
lanraragi-database:

Nginx configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
user  nginx;
worker_processes auto;

error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;

keepalive_timeout 65;

server {
listen 443 default_server ssl http2;
listen [::]:443 ssl http2;

ssl_certificate /etc/nginx/ssl/nginx_server.crt;
ssl_certificate_key /etc/nginx/ssl/nginx_server.key;

auth_basic "Login to view any content";
auth_basic_user_file /etc/nginx/auth/.htpasswd-lanraragi;

location / {
# Access directly through service name in the same network
# No need to map any port for lanraragi
proxy_pass http://lanraragi:3000;
proxy_http_version 1.1;

proxy_set_header Upgrade $http_upgrade;
http2_push_preload on;
}
}
}

Lanraragi’s own routing strategy cannot be changed, which means / must be mapped as-is to Lanraragi’s URI. In other words, Lanraragi needs a separate Nginx instance.

From a computational resources perspective, this isn’t a big deal, as one Nginx instance uses less than 20 MB of memory.

If each existing software gets its own Nginx, these Nginx instances can share a single SSL certificate. After all, when accessing the Linux host, it tells Nginx instances from each other by port, and the certificate is bound to IP/domain. Although this is a solution, I’m not sure if it’s the best practice. However, in a home LAN scenario, it is clearly sufficient.

References