mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-25 18:40:06 -04:00
Replace nginx with Caddy for TLS termination and reverse proxying.
- Replace nginx with Caddy as the web server for both Docker and native (RPi/Debian) deployments. Caddy handles HTTPS termination, reverse proxying to all services (API, React app, Controller, Kiwix, Help, Map), and serves the media/download file browsers. - Generate a Root CA and signed leaf certificates for HTTPS. The Root CA uses Name Constraints to restrict signing to local addresses only (.local, localhost, and RFC 1918 private IP ranges), mitigating MITM risk if the CA key is compromised. - Add an HTTP landing page that guides users through one-time CA certificate installation on iOS, Android, macOS, Windows, Linux, and Firefox, with auto-redirect when the cert is already trusted. - Add trailing-slash redirects for /controller, /docs, /media, and /download so bare paths don't fall through to the React catch-all. - Add a Root CA download link on the Settings page under Configs. - Update help.sh diagnostics for IPv6 listener detection and HTTP/2 status codes. - Remove all nginx config files and the https_proxy Docker container. - Bump wrolpi-help submodule with certificate documentation.
This commit is contained in:
parent
dd270b37dc
commit
fbb71e012d
43 changed files with 881 additions and 583 deletions
|
|
@ -27,6 +27,7 @@ network so that any user with a laptop/tablet/phone can connect and use the libr
|
|||
* [Demo](#demo)
|
||||
* [Download](#download)
|
||||
* [Try WROLPi](#try-wrolpi)
|
||||
* [Docker Documentation](docker/README.md)
|
||||
* [Debian 12 Install](#debian-12-install)
|
||||
* [Raspberry Pi Install](#raspberrypi-install)
|
||||
* [Charter](#charter)
|
||||
|
|
@ -93,6 +94,8 @@ You can try out WROLPi by running the docker containers.
|
|||
10. Browse to WROLPi.
|
||||
* https://0.0.0.0:8443
|
||||
|
||||
More Docker documentation is available in [docker/README.md](docker/README.md)
|
||||
|
||||
# Debian 12 Install
|
||||
|
||||
Steps necessary to initialize your WROLPi after installing the Debian image from wrolpi.org
|
||||
|
|
|
|||
|
|
@ -814,6 +814,15 @@ export function SettingsPage() {
|
|||
saveConfig={saveConfig}
|
||||
fetchConfigs={fetchConfigs}
|
||||
/>
|
||||
|
||||
<Divider/>
|
||||
|
||||
<Header as='h3'>Root CA Certificate</Header>
|
||||
<p>Install the WROLPi Root CA certificate on your devices to trust all HTTPS connections to this WROLPi.
|
||||
You only need to do this once per device.</p>
|
||||
<a href='/ca.crt' download='wrolpi-ca.crt'>
|
||||
<Button primary><Icon name='download'/> Download Root CA Certificate</Button>
|
||||
</a>
|
||||
</Segment>;
|
||||
|
||||
return <Container fluid>
|
||||
|
|
|
|||
|
|
@ -102,8 +102,8 @@ DEFAULT_CONFIG = {
|
|||
"description": "PostgreSQL database",
|
||||
},
|
||||
{
|
||||
"name": "nginx",
|
||||
"systemd_name": "nginx",
|
||||
"name": "caddy",
|
||||
"systemd_name": "caddy",
|
||||
"port": 80,
|
||||
"viewable": True,
|
||||
"use_https": False,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ CONTAINER_PREFIX = os.environ.get("COMPOSE_PROJECT_NAME", "wrolpi")
|
|||
# Containers that should not have a "View" button (non-HTTP services)
|
||||
NON_VIEWABLE_CONTAINERS = {"db", "postgres", "postgresql", "redis", "memcached"}
|
||||
|
||||
# Containers that should use HTTPS (in addition to *_https suffix detection)
|
||||
# Containers that should use HTTPS
|
||||
HTTPS_CONTAINERS = {"web"}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class TestManagedServicesConfig:
|
|||
{"systemd_name": "wrolpi-api-dev", "port": 8081, "viewable": True, "show_only_when_running": True}),
|
||||
("wrolpi-app", {"port": 3000, "viewable": False}),
|
||||
("wrolpi-app-dev", {"port": 3000, "viewable": False, "show_only_when_running": True}),
|
||||
("nginx", {"port": 80}),
|
||||
("caddy", {"port": 80}),
|
||||
("postgresql", {"port": 5432, "viewable": False}),
|
||||
("wrolpi-upgrade", {"port": None, "viewable": False, "show_only_when_running": True}),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ class TestGetAllContainersStatus:
|
|||
assert result[0].get("use_https", False) is False
|
||||
|
||||
def test_web_container_uses_https(self):
|
||||
"""Web container (nginx proxy) should have use_https=True."""
|
||||
"""Web container (Caddy proxy) should have use_https=True."""
|
||||
mock_container = mock.Mock()
|
||||
mock_container.name = f"{CONTAINER_PREFIX}-web-1"
|
||||
mock_container.status = "running"
|
||||
|
|
|
|||
|
|
@ -56,14 +56,17 @@ python3 -m venv /opt/wrolpi/controller/venv
|
|||
/opt/wrolpi/controller/venv/bin/pip install -r /opt/wrolpi/controller/requirements.txt
|
||||
echo "Controller pre-installed"
|
||||
|
||||
# Configure nginx.
|
||||
cp /opt/wrolpi/etc/raspberrypios/nginx.conf /etc/nginx/nginx.conf
|
||||
# Install Caddy from official apt repo.
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
|
||||
apt-get update
|
||||
apt-get install -y caddy
|
||||
|
||||
# Configure Caddy.
|
||||
mkdir -p /etc/caddy
|
||||
cp /opt/wrolpi/etc/raspberrypios/Caddyfile /etc/caddy/Caddyfile
|
||||
cp /opt/wrolpi/etc/raspberrypios/50x.html /var/www/50x.html
|
||||
# Copy wrolpi.conf which defines the location blocks for reverse proxy.
|
||||
[ -f /etc/nginx/conf.d/default.conf ] && rm /etc/nginx/conf.d/default.conf
|
||||
cp /opt/wrolpi/etc/raspberrypios/wrolpi.conf /etc/nginx/conf.d/wrolpi.conf
|
||||
# Remove default site (WROLPi nginx.conf doesn't use sites-enabled).
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
cp /opt/wrolpi/etc/raspberrypios/landing.html /var/www/landing.html
|
||||
|
||||
# WROLPi needs a few privileged commands.
|
||||
cp /opt/wrolpi/etc/raspberrypios/90-wrolpi /etc/sudoers.d/90-wrolpi
|
||||
|
|
@ -88,6 +91,7 @@ mkdir /media/wrolpi
|
|||
/opt/wrolpi/scripts/install_help_service.sh
|
||||
|
||||
cp /opt/wrolpi/etc/raspberrypios/wrolpi-*.service /etc/systemd/system/
|
||||
cp /opt/wrolpi/etc/raspberrypios/wrolpi-*.timer /etc/systemd/system/
|
||||
cp /opt/wrolpi/etc/raspberrypios/wrolpi.target /etc/systemd/system/
|
||||
cp /opt/wrolpi/etc/raspberrypios/renderd.conf /etc/renderd.conf
|
||||
|
||||
|
|
@ -102,10 +106,14 @@ systemctl enable NetworkManager
|
|||
|
||||
# Enable iperf3 for speed testing.
|
||||
systemctl enable iperf3
|
||||
# Enable first startup script to generate certificates and start nginx.
|
||||
# Enable first startup script to generate certificates and start Caddy.
|
||||
systemctl enable wrolpi-first-startup.service
|
||||
# Enable Controller service to start on boot.
|
||||
systemctl enable wrolpi-controller.service
|
||||
# Enable weekly certificate renewal check.
|
||||
systemctl enable wrolpi-cert-renew.timer
|
||||
# Enable weekly certificate renewal check.
|
||||
systemctl enable wrolpi-cert-renew.timer
|
||||
|
||||
# Protect blob files from accidental modification or deletion.
|
||||
# These files are critical for initializing WROLPi.
|
||||
|
|
|
|||
|
|
@ -66,8 +66,6 @@ lua5.1
|
|||
mapnik-utils
|
||||
netcat-openbsd
|
||||
network-manager
|
||||
nginx-doc
|
||||
nginx-full
|
||||
nicstat
|
||||
nodejs
|
||||
npm
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ services:
|
|||
interval: 1m
|
||||
user: '${UID-1000}:${GID-1000}'
|
||||
environment:
|
||||
# Force React to use nginx port to connect to it's websocket.
|
||||
# Force React to use Caddy port to connect to it's websocket.
|
||||
- WDS_SOCKET_PORT=0
|
||||
|
||||
map: # https://github.com/Overv/openstreetmap-tile-server
|
||||
|
|
@ -119,39 +119,33 @@ services:
|
|||
test: [ 'CMD-SHELL', 'curl http://127.0.0.1:80' ]
|
||||
interval: 1m
|
||||
|
||||
map_https: # HTTPS for map tile server
|
||||
depends_on:
|
||||
- map
|
||||
environment:
|
||||
- UPSTREAM_HOST=map
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/https_proxy/Dockerfile
|
||||
ports:
|
||||
- '8084:443'
|
||||
volumes:
|
||||
- './docker/map/nginx.map.conf:/etc/nginx/nginx.conf'
|
||||
healthcheck:
|
||||
test: [ 'CMD-SHELL', 'curl http://127.0.0.1:443' ]
|
||||
interval: 1m
|
||||
|
||||
web:
|
||||
web: # Caddy reverse proxy - handles HTTPS for all services.
|
||||
depends_on:
|
||||
- app
|
||||
- zim
|
||||
- help
|
||||
- map
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/web/Dockerfile
|
||||
ports:
|
||||
- ${WEB_HOST-0.0.0.0}:${WEB_PORT-8080}:80
|
||||
- ${WEB_HOST-0.0.0.0}:${WEB_HTTPS_PORT-8443}:443
|
||||
- '8084:8084'
|
||||
- '8085:8085'
|
||||
- '8086:8086'
|
||||
volumes:
|
||||
- './docker/web/nginx.dev.conf:/etc/nginx/nginx.conf'
|
||||
- './docker/web/wrolpi.dev.conf:/etc/nginx/conf.d/wrolpi.conf'
|
||||
- './icon.ico:/etc/nginx/favicon.ico'
|
||||
- './icon.png:/etc/nginx/icon.png'
|
||||
- './docker/web/Caddyfile:/etc/caddy/Caddyfile'
|
||||
- './icon.ico:/etc/caddy/favicon.ico'
|
||||
- './icon.png:/etc/caddy/icon.png'
|
||||
- './test:/opt/media'
|
||||
environment:
|
||||
- MEDIA_DIRECTORY=/opt/media
|
||||
# Set EXTRA_SANS to include the host's LAN IP in the TLS certificate.
|
||||
# Example: EXTRA_SANS=IP:10.0.0.5
|
||||
- EXTRA_SANS=${EXTRA_SANS:-}
|
||||
healthcheck:
|
||||
test: [ 'CMD-SHELL', 'curl http://127.0.0.1:80' ]
|
||||
test: [ 'CMD-SHELL', 'wget --spider http://127.0.0.1:80 || exit 1' ]
|
||||
interval: 1m
|
||||
|
||||
zim: # kiwix-serve
|
||||
|
|
@ -165,22 +159,6 @@ services:
|
|||
test: [ 'CMD-SHELL', 'curl http://127.0.0.1:80' ]
|
||||
interval: 1m
|
||||
|
||||
zim_https: # HTTPS for Zim service.
|
||||
depends_on:
|
||||
- zim
|
||||
environment:
|
||||
- UPSTREAM_HOST=zim
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/https_proxy/Dockerfile
|
||||
ports:
|
||||
- '8085:443'
|
||||
volumes:
|
||||
- './docker/zim/nginx.zim.conf:/etc/nginx/nginx.conf'
|
||||
healthcheck:
|
||||
test: [ 'CMD-SHELL', 'curl http://127.0.0.1:443' ]
|
||||
interval: 1m
|
||||
|
||||
help: # The WROLPi Help documentation.
|
||||
build:
|
||||
context: .
|
||||
|
|
@ -191,22 +169,6 @@ services:
|
|||
test: [ 'CMD-SHELL', 'curl http://127.0.0.1:8086' ]
|
||||
interval: 1m
|
||||
|
||||
help_https: # HTTPS for Help service.
|
||||
depends_on:
|
||||
- help
|
||||
environment:
|
||||
- UPSTREAM_HOST=help
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/https_proxy/Dockerfile
|
||||
ports:
|
||||
- '8086:443'
|
||||
volumes:
|
||||
- './docker/help/nginx.help.conf:/etc/nginx/nginx.conf'
|
||||
healthcheck:
|
||||
test: [ 'CMD-SHELL', 'curl http://127.0.0.1:443' ]
|
||||
interval: 1m
|
||||
|
||||
volumes:
|
||||
openstreetmap-data:
|
||||
external: true
|
||||
|
|
|
|||
127
docker/README.md
Normal file
127
docker/README.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# WROLPi Docker
|
||||
|
||||
Development and testing environment using Docker containers.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/lrnselfreliance/wrolpi.git
|
||||
cd wrolpi
|
||||
git submodule update --init
|
||||
docker-compose build --parallel
|
||||
docker volume create --name=openstreetmap-data
|
||||
docker volume create --name=openstreetmap-rendered-tiles
|
||||
docker-compose up -d db
|
||||
docker-compose run --rm api db upgrade
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
Browse to https://localhost:8443
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Port(s) | Description |
|
||||
|----------------|--------------------------------------|-----------------------------|
|
||||
| **web** | 8080 (HTTP), 8443 (HTTPS), 8084-8086 | Caddy reverse proxy |
|
||||
| **api** | 8081 | Sanic API server |
|
||||
| **app** | (via web) | React frontend |
|
||||
| **controller** | 8087 | System management (FastAPI) |
|
||||
| **db** | 5432 | PostgreSQL 12 |
|
||||
| **archive** | 8083 | SingleFile/Readability |
|
||||
| **zim** | (via web:8085) | Kiwix |
|
||||
| **help** | (via web:8086) | MkDocs documentation |
|
||||
| **map** | (via web:8084) | OpenStreetMap tile server |
|
||||
|
||||
## TLS Certificates
|
||||
|
||||
WROLPi uses a two-tier PKI for HTTPS. A **root CA** is generated once and persists
|
||||
in the media directory. A **leaf certificate** is regenerated on every container start
|
||||
to pick up hostname and IP changes.
|
||||
|
||||
### How it works
|
||||
|
||||
1. On first start, the `web` container generates a root CA at `/opt/media/config/ssl/`.
|
||||
2. A leaf cert signed by the root CA is created at `/etc/ssl/caddy/` inside the container.
|
||||
3. Caddy serves HTTPS on ports 443, 8084, 8085, and 8086 using the leaf cert.
|
||||
4. Port 80 serves an HTTP landing page where users can download the root CA.
|
||||
|
||||
### Trusting the certificate
|
||||
|
||||
1. Browse to `http://<host-ip>:8080` to see the landing page with download link and
|
||||
platform-specific trust instructions.
|
||||
2. Download and install the root CA on your device.
|
||||
3. All HTTPS ports will work without browser warnings.
|
||||
|
||||
The root CA only needs to be trusted once. Leaf certs can be regenerated (hostname
|
||||
change, container rebuild) without needing to re-trust.
|
||||
|
||||
### Name Constraints
|
||||
|
||||
The root CA is restricted to local/private networks only via X.509 Name Constraints.
|
||||
Even if the CA key were compromised, it cannot sign certificates for public domains.
|
||||
|
||||
Permitted:
|
||||
|
||||
- DNS: `*.local`, `localhost`
|
||||
- IPv4: `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`
|
||||
- IPv6: `::1`, `fc00::/7` (unique local), `fe80::/10` (link-local)
|
||||
|
||||
### EXTRA_SANS
|
||||
|
||||
The Docker container cannot discover the host's real LAN IP. Set `EXTRA_SANS` in a
|
||||
`.env` file so the leaf certificate includes your host's IP and hostname:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
EXTRA_SANS=IP:10.0.0.5,DNS:myhost.local
|
||||
```
|
||||
|
||||
Each entry must have a type prefix (`IP:` or `DNS:`). Multiple entries are
|
||||
comma-separated. After changing `EXTRA_SANS`, restart the web container:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build web
|
||||
```
|
||||
|
||||
### Regenerating the root CA
|
||||
|
||||
If you need to regenerate the root CA (e.g. after updating the certificate
|
||||
generation script), delete the old CA files and rebuild:
|
||||
|
||||
```bash
|
||||
rm test/config/ssl/ca.crt test/config/ssl/ca.key
|
||||
docker compose up -d --build web
|
||||
```
|
||||
|
||||
All devices will need to re-download and re-trust the new CA from
|
||||
`http://<host-ip>:8080/ca.crt`.
|
||||
|
||||
## Volumes
|
||||
|
||||
The `test/` directory is mounted to `/opt/media` inside containers, serving as the
|
||||
media directory for development. Certificate files, configs, and downloaded content
|
||||
are stored here.
|
||||
|
||||
Two external volumes are required for the map service:
|
||||
|
||||
```bash
|
||||
docker volume create --name=openstreetmap-data
|
||||
docker volume create --name=openstreetmap-rendered-tiles
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|-------------------|----------------|----------------------------------------------|
|
||||
| `MEDIA_DIRECTORY` | `/opt/media` | Media directory inside containers |
|
||||
| `EXTRA_SANS` | (empty) | Additional SANs for the TLS leaf certificate |
|
||||
| `WEB_HOST` | `0.0.0.0` | Host bind address for the web service |
|
||||
| `WEB_PORT` | `8080` | HTTP port |
|
||||
| `WEB_HTTPS_PORT` | `8443` | HTTPS port |
|
||||
| `REACT_APP_API` | `0.0.0.0:8081` | API bind address |
|
||||
| `UID` | `1000` | User ID for api/app containers |
|
||||
| `GID` | `1000` | Group ID for api/app containers |
|
||||
|
||||
## Upgrading
|
||||
|
||||
See [UPGRADE.md](../UPGRADE.md) for upgrade instructions.
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
events {}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 443 ssl;
|
||||
|
||||
ssl_certificate /cert.crt;
|
||||
ssl_certificate_key /cert.key;
|
||||
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
|
||||
location / {
|
||||
proxy_pass http://help:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header 'Access-Control-Allow-Origin' '*';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 0bc20aa33e03f35f2da62ecd9460636fc8398b1c
|
||||
Subproject commit f8164d905310670b95f63d08bb9254f7d4359f7d
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
# This Dockerfile is used as a reverse proxy to serve a HTTPS nieve service with HTTPS.
|
||||
FROM nginx:latest AS https_proxy
|
||||
|
||||
# Create a self-signed cert and key pair for HTTPS.
|
||||
RUN openssl genrsa -out /cert.key 2048
|
||||
RUN openssl req -new -x509 -nodes -key /cert.key -out /cert.crt -days 3650 \
|
||||
-subj "/C=US/ST=State/L=City/O=Org/OU=WROLPi/CN=wrolpi.local"
|
||||
RUN chmod 777 /cert.key /cert.crt
|
||||
|
||||
# Apply docker-compose environment variables to config
|
||||
ENV UPSTREAM_HOST=you-must-change-this-in-docker-compose
|
||||
ENV UPSTREAM_PORT=80
|
||||
COPY docker/https_proxy/nginx.proxy.conf.template /etc/nginx/nginx.conf
|
||||
RUN envsubst < /etc/nginx/nginx.conf > /etc/nginx/nginx.conf
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
events {}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 443 ssl;
|
||||
|
||||
ssl_certificate /cert.crt;
|
||||
ssl_certificate_key /cert.key;
|
||||
|
||||
location / {
|
||||
proxy_pass http://${UPSTREAM_HOST}:${UPSTREAM_PORT};
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
events {}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 443 ssl;
|
||||
|
||||
ssl_certificate /cert.crt;
|
||||
ssl_certificate_key /cert.key;
|
||||
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
|
||||
location / {
|
||||
proxy_pass http://map:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header 'Access-Control-Allow-Origin' '*';
|
||||
}
|
||||
}
|
||||
}
|
||||
119
docker/web/Caddyfile
Normal file
119
docker/web/Caddyfile
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# Caddy config for WROLPi services (Docker).
|
||||
|
||||
{
|
||||
# No ACME - WROLPi is designed for offline use.
|
||||
auto_https disable_redirects
|
||||
}
|
||||
|
||||
:80 {
|
||||
handle / {
|
||||
root * /var/www
|
||||
rewrite * /landing.html
|
||||
file_server
|
||||
}
|
||||
handle /ca.crt {
|
||||
root * /opt/media/config/ssl
|
||||
header Content-Type "application/x-x509-ca-cert"
|
||||
header Content-Disposition "attachment; filename=\"wrolpi-ca.crt\""
|
||||
file_server
|
||||
}
|
||||
handle {
|
||||
redir https://{host}:8443{uri} permanent
|
||||
}
|
||||
}
|
||||
|
||||
:443 {
|
||||
tls /etc/ssl/caddy/cert.crt /etc/ssl/caddy/cert.key
|
||||
|
||||
handle /favicon.ico {
|
||||
root * /etc/caddy
|
||||
rewrite * /favicon.ico
|
||||
file_server
|
||||
}
|
||||
|
||||
handle /apple-touch-icon.png {
|
||||
root * /etc/caddy
|
||||
rewrite * /icon.png
|
||||
file_server
|
||||
}
|
||||
|
||||
# WebSocket for React dev server.
|
||||
handle /ws {
|
||||
reverse_proxy app:3000
|
||||
}
|
||||
|
||||
# Controller - system management API and UI.
|
||||
handle_path /controller/* {
|
||||
reverse_proxy controller:8087
|
||||
}
|
||||
handle /controller {
|
||||
redir * /controller/ 301
|
||||
}
|
||||
|
||||
# Main API.
|
||||
handle /api/* {
|
||||
reverse_proxy api:8081
|
||||
}
|
||||
|
||||
# API docs.
|
||||
handle /docs/* {
|
||||
reverse_proxy api:8081
|
||||
}
|
||||
handle /docs {
|
||||
redir * /docs/ 301
|
||||
}
|
||||
|
||||
# Static media files with directory browsing.
|
||||
handle_path /media/* {
|
||||
root * /opt/media
|
||||
file_server browse
|
||||
}
|
||||
handle /media {
|
||||
redir * /media/ 301
|
||||
}
|
||||
|
||||
# Downloads with Content-Disposition header.
|
||||
handle_path /download/* {
|
||||
root * /opt/media
|
||||
header Content-Disposition "attachment"
|
||||
file_server browse
|
||||
}
|
||||
handle /download {
|
||||
redir * /download/ 301
|
||||
}
|
||||
|
||||
# React app with fallback to Controller.
|
||||
handle {
|
||||
reverse_proxy app:3000 {
|
||||
@bad status 502 503 504
|
||||
handle_response @bad {
|
||||
header Location /controller/?fallback=true
|
||||
respond "" 302
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Kiwix HTTPS proxy (replaces zim_https container).
|
||||
:8085 {
|
||||
tls /etc/ssl/caddy/cert.crt /etc/ssl/caddy/cert.key
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
reverse_proxy zim:80
|
||||
}
|
||||
|
||||
# Help HTTPS proxy (replaces help_https container).
|
||||
:8086 {
|
||||
tls /etc/ssl/caddy/cert.crt /etc/ssl/caddy/cert.key
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
reverse_proxy help:80
|
||||
}
|
||||
|
||||
# Map HTTPS proxy (replaces map_https container).
|
||||
:8084 {
|
||||
tls /etc/ssl/caddy/cert.crt /etc/ssl/caddy/cert.key
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
reverse_proxy map:80
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
FROM nginx:latest
|
||||
FROM caddy:latest
|
||||
|
||||
# Create a self-signed cert and key pair for HTTPS.
|
||||
RUN openssl genrsa -out /cert.key 2048
|
||||
RUN openssl req -new -x509 -nodes -key /cert.key -out /cert.crt -days 3650 \
|
||||
-subj "/C=US/ST=State/L=City/O=Org/OU=WROLPi/CN=wrolpi.local"
|
||||
RUN chmod 777 /cert.key /cert.crt
|
||||
RUN apk add --no-cache openssl bash
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY docker/web/nginx.dev.conf /etc/nginx/nginx.conf
|
||||
COPY docker/web/wrolpi.dev.conf /etc/nginx/conf.d/wrolpi.conf
|
||||
# Copy scripts and static files.
|
||||
COPY docker/web/Caddyfile /etc/caddy/Caddyfile
|
||||
COPY docker/web/entrypoint.sh /entrypoint.sh
|
||||
COPY scripts/generate_certificates.sh /opt/wrolpi/scripts/generate_certificates.sh
|
||||
COPY etc/raspberrypios/landing.html /var/www/landing.html
|
||||
|
||||
RUN chmod +x /entrypoint.sh /opt/wrolpi/scripts/generate_certificates.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
|
|
|||
11
docker/web/entrypoint.sh
Normal file
11
docker/web/entrypoint.sh
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
export MEDIA_DIRECTORY="${MEDIA_DIRECTORY:-/opt/media}"
|
||||
export CERT_DIR="/etc/ssl/caddy"
|
||||
|
||||
# EXTRA_SANS can be set to include additional SANs in the leaf certificate.
|
||||
# This is needed in Docker because the container cannot discover the host's
|
||||
# real LAN IP. Example: EXTRA_SANS=IP:10.0.0.5,DNS:myhost.local
|
||||
export EXTRA_SANS="${EXTRA_SANS:-}"
|
||||
|
||||
/opt/wrolpi/scripts/generate_certificates.sh
|
||||
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
events {
|
||||
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
client_max_body_size 0;
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
charset utf-8;
|
||||
|
||||
include mime.types;
|
||||
types {
|
||||
# Custom types for ebooks.
|
||||
application/x-mobipocket-ebook mobi;
|
||||
application/epub+zip epub;
|
||||
# Configs
|
||||
text/yaml yaml yml;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _; # Default server for requests without "Host" header
|
||||
|
||||
location / {
|
||||
return 301 https://$host:8443$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
proxy_pass_header Host;
|
||||
|
||||
# Self-signed cert from Dockerfile build.
|
||||
ssl_certificate /cert.crt;
|
||||
ssl_certificate_key /cert.key;
|
||||
|
||||
include /etc/nginx/conf.d/*;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
# Nginx config for the WROLPi services.
|
||||
|
||||
#
|
||||
# App
|
||||
#
|
||||
|
||||
location /favicon.ico {
|
||||
alias /etc/nginx/favicon.ico;
|
||||
}
|
||||
|
||||
location /apple-touch-icon.png {
|
||||
alias /etc/nginx/icon.png;
|
||||
}
|
||||
|
||||
location / {
|
||||
# React APP with Controller fallback
|
||||
proxy_pass http://app:3000;
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 503 504 = @react_fallback;
|
||||
rewrite $1 break;
|
||||
}
|
||||
|
||||
location @react_fallback {
|
||||
return 302 /controller/?fallback=true;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://app:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
}
|
||||
|
||||
#
|
||||
# Controller - system management API and UI
|
||||
# All Controller endpoints are under /controller/*
|
||||
#
|
||||
location /controller {
|
||||
rewrite ^/controller(.*)$ $1 break;
|
||||
proxy_pass http://controller:8087;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
#
|
||||
# Main API - handled by the API service (port 8081)
|
||||
#
|
||||
|
||||
location /api {
|
||||
proxy_pass http://api:8081;
|
||||
rewrite /^api(.*)$ $1 break;
|
||||
}
|
||||
|
||||
location /docs {
|
||||
proxy_pass http://api:8081;
|
||||
rewrite /^docs(.*)$ $1 break;
|
||||
}
|
||||
|
||||
location /media {
|
||||
# The media files mounted in the docker volume
|
||||
sendfile on;
|
||||
sendfile_max_chunk 1m;
|
||||
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
autoindex on;
|
||||
autoindex_exact_size off;
|
||||
alias /opt/media;
|
||||
}
|
||||
|
||||
location /download {
|
||||
# Assume any request here is to download a file from the media directory.
|
||||
if ( $request_filename ~ '^.*/(.+)$' ) {
|
||||
# Always include the filename for downloads.
|
||||
add_header Content-Disposition 'attachment; filename="$1"';
|
||||
}
|
||||
|
||||
sendfile on;
|
||||
sendfile_max_chunk 1m;
|
||||
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
autoindex on;
|
||||
autoindex_exact_size off;
|
||||
alias /opt/media;
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
events {}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 443 ssl;
|
||||
|
||||
ssl_certificate /cert.crt;
|
||||
ssl_certificate_key /cert.key;
|
||||
|
||||
location / {
|
||||
proxy_pass http://zim:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header 'Access-Control-Allow-Origin' '*';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<VirtualHost *:8084>
|
||||
<VirtualHost *:9084>
|
||||
# The ServerName directive sets the request scheme, hostname and port that
|
||||
# the server uses to identify itself. This is used when creating
|
||||
# redirection URLs. In the context of virtual hosts, the ServerName
|
||||
|
|
@ -19,10 +19,6 @@
|
|||
|
||||
DocumentRoot /var/www/html
|
||||
|
||||
SSLEngine on
|
||||
SSLCertificateFile /etc/nginx/cert.crt
|
||||
SSLCertificateKeyFile /etc/nginx/cert.key
|
||||
|
||||
Header set Access-Control-Allow-Origin "*"
|
||||
Header set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT"
|
||||
Header set Access-Control-Allow-Headers "Content-Type"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<VirtualHost *:8084>
|
||||
<VirtualHost *:9084>
|
||||
# The ServerName directive sets the request scheme, hostname and port that
|
||||
# the server uses to identify itself. This is used when creating
|
||||
# redirection URLs. In the context of virtual hosts, the ServerName
|
||||
|
|
@ -19,10 +19,6 @@
|
|||
|
||||
DocumentRoot /var/www/html
|
||||
|
||||
SSLEngine on
|
||||
SSLCertificateFile /etc/nginx/cert.crt
|
||||
SSLCertificateKeyFile /etc/nginx/cert.key
|
||||
|
||||
Header set Access-Control-Allow-Origin "*"
|
||||
Header set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT"
|
||||
Header set Access-Control-Allow-Headers "Content-Type"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<h1>Error!</h1>
|
||||
|
||||
<p>nginx could not find WROLPi! Are your services running?</p>
|
||||
<p>The web proxy could not find WROLPi! Are your services running?</p>
|
||||
|
||||
<p>Start by viewing status of your services at the
|
||||
<a id="controller_link" href="http://127.0.0.1:8087">Controller</a>
|
||||
|
|
|
|||
132
etc/raspberrypios/Caddyfile
Normal file
132
etc/raspberrypios/Caddyfile
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Caddy config for WROLPi services (RPi/Debian bare metal).
|
||||
|
||||
{
|
||||
# No ACME - WROLPi is designed for offline use.
|
||||
auto_https disable_redirects
|
||||
}
|
||||
|
||||
:80 {
|
||||
handle / {
|
||||
root * /var/www
|
||||
rewrite * /landing.html
|
||||
file_server
|
||||
}
|
||||
handle /ca.crt {
|
||||
root * /media/wrolpi/config/ssl
|
||||
header Content-Type "application/x-x509-ca-cert"
|
||||
header Content-Disposition "attachment; filename=\"wrolpi-ca.crt\""
|
||||
file_server
|
||||
}
|
||||
handle {
|
||||
redir https://{host}{uri} permanent
|
||||
}
|
||||
}
|
||||
|
||||
:443 {
|
||||
tls /etc/ssl/wrolpi/cert.crt /etc/ssl/wrolpi/cert.key
|
||||
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
header Access-Control-Allow-Headers "Content-Type, Accept"
|
||||
|
||||
handle /favicon.ico {
|
||||
root * /opt/wrolpi
|
||||
rewrite * /icon.ico
|
||||
file_server
|
||||
}
|
||||
|
||||
handle /apple-touch-icon.png {
|
||||
root * /opt/wrolpi
|
||||
rewrite * /icon.png
|
||||
file_server
|
||||
}
|
||||
|
||||
# WebSocket for React dev server.
|
||||
handle /ws {
|
||||
reverse_proxy 0.0.0.0:3000
|
||||
}
|
||||
|
||||
# Controller - system management API and UI.
|
||||
handle_path /controller/* {
|
||||
reverse_proxy 0.0.0.0:8087
|
||||
}
|
||||
handle /controller {
|
||||
redir * /controller/ 301
|
||||
}
|
||||
|
||||
# Main API.
|
||||
handle /api/* {
|
||||
reverse_proxy 0.0.0.0:8081
|
||||
}
|
||||
|
||||
# API docs.
|
||||
handle /docs/* {
|
||||
reverse_proxy 0.0.0.0:8081
|
||||
}
|
||||
handle /docs {
|
||||
redir * /docs/ 301
|
||||
}
|
||||
|
||||
# Static media files with directory browsing.
|
||||
handle_path /media/* {
|
||||
root * /media/wrolpi
|
||||
file_server browse
|
||||
}
|
||||
handle /media {
|
||||
redir * /media/ 301
|
||||
}
|
||||
|
||||
# Downloads with Content-Disposition header.
|
||||
handle_path /download/* {
|
||||
root * /media/wrolpi
|
||||
header Content-Disposition "attachment"
|
||||
file_server browse
|
||||
}
|
||||
handle /download {
|
||||
redir * /download/ 301
|
||||
}
|
||||
|
||||
# React app with fallback to Controller.
|
||||
handle {
|
||||
reverse_proxy 0.0.0.0:3000 {
|
||||
@bad status 502 503 504
|
||||
handle_response @bad {
|
||||
header Location /controller/?fallback=true
|
||||
respond "" 302
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle_errors {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
header Access-Control-Allow-Headers "Content-Type, Accept"
|
||||
root * /var/www
|
||||
rewrite * /50x.html
|
||||
file_server
|
||||
}
|
||||
}
|
||||
|
||||
# Kiwix HTTPS proxy.
|
||||
:8085 {
|
||||
tls /etc/ssl/wrolpi/cert.crt /etc/ssl/wrolpi/cert.key
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
reverse_proxy 0.0.0.0:9085
|
||||
}
|
||||
|
||||
# Help HTTPS proxy.
|
||||
:8086 {
|
||||
tls /etc/ssl/wrolpi/cert.crt /etc/ssl/wrolpi/cert.key
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
reverse_proxy 0.0.0.0:9086
|
||||
}
|
||||
|
||||
# Map HTTPS proxy (Apache2 HTTP-only on 9084).
|
||||
:8084 {
|
||||
tls /etc/ssl/wrolpi/cert.crt /etc/ssl/wrolpi/cert.key
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
reverse_proxy 0.0.0.0:9084
|
||||
}
|
||||
174
etc/raspberrypios/landing.html
Normal file
174
etc/raspberrypios/landing.html
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Welcome to WROLPi</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #1a1a2e; color: #eee; min-height: 100vh;
|
||||
display: flex; justify-content: center; padding: 2rem 1rem;
|
||||
}
|
||||
.container { max-width: 640px; width: 100%; }
|
||||
h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #fff; }
|
||||
.subtitle { color: #aaa; margin-bottom: 2rem; }
|
||||
.card {
|
||||
background: #16213e; border-radius: 12px; padding: 1.5rem;
|
||||
margin-bottom: 1.5rem; border: 1px solid #1a3a5c;
|
||||
}
|
||||
.card h2 { font-size: 1.1rem; margin-bottom: 0.75rem; color: #4cc9f0; }
|
||||
.card p { line-height: 1.6; color: #ccc; margin-bottom: 1rem; }
|
||||
.btn {
|
||||
display: inline-block; padding: 0.75rem 1.5rem; border-radius: 8px;
|
||||
text-decoration: none; font-weight: 600; font-size: 1rem;
|
||||
cursor: pointer; border: none; text-align: center;
|
||||
}
|
||||
.btn-primary { background: #4cc9f0; color: #1a1a2e; }
|
||||
.btn-primary:hover { background: #3ab8df; }
|
||||
.btn-continue { background: #2ecc71; color: #fff; width: 100%; font-size: 1.1rem; }
|
||||
.btn-continue:hover { background: #27ae60; }
|
||||
details { margin-bottom: 0.75rem; }
|
||||
summary {
|
||||
cursor: pointer; padding: 0.5rem 0; font-weight: 600;
|
||||
color: #4cc9f0; list-style: none;
|
||||
}
|
||||
summary::before { content: "\25b6\00a0"; font-size: 0.8rem; }
|
||||
details[open] > summary::before { content: "\25bc\00a0"; }
|
||||
.instructions { padding: 0.75rem 0 0.25rem 0.5rem; color: #bbb; line-height: 1.7; }
|
||||
.instructions ol { padding-left: 1.25rem; }
|
||||
.instructions li { margin-bottom: 0.3rem; }
|
||||
code { background: #0f3460; padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
|
||||
#auto-redirect { display: none; text-align: center; color: #2ecc71; margin-bottom: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Welcome to WROLPi</h1>
|
||||
<p class="subtitle">Your offline library needs a one-time certificate setup.</p>
|
||||
|
||||
<div id="auto-redirect" class="card">
|
||||
<p>Certificate already trusted! Redirecting...</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Step 1: Download the Root CA Certificate</h2>
|
||||
<p>Install this certificate on your device to trust all WROLPi HTTPS services permanently.</p>
|
||||
<a class="btn btn-primary" href="/ca.crt" download="wrolpi-ca.crt">Download WROLPi Root CA</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Step 2: Trust the Certificate</h2>
|
||||
<p>Follow the instructions for your device:</p>
|
||||
|
||||
<details>
|
||||
<summary>iOS / iPadOS</summary>
|
||||
<div class="instructions">
|
||||
<ol>
|
||||
<li>Tap the download button above, then tap <strong>Allow</strong> when prompted.</li>
|
||||
<li>Open <strong>Settings</strong> → <strong>General</strong> → <strong>VPN & Device Management</strong>.</li>
|
||||
<li>Tap <strong>WROLPi Root CA</strong> → <strong>Install</strong> → enter your passcode.</li>
|
||||
<li>Go to <strong>Settings</strong> → <strong>General</strong> → <strong>About</strong> → <strong>Certificate Trust Settings</strong>.</li>
|
||||
<li>Enable full trust for <strong>WROLPi Root CA</strong>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Android</summary>
|
||||
<div class="instructions">
|
||||
<ol>
|
||||
<li>Tap the download button above.</li>
|
||||
<li>Open <strong>Settings</strong> → <strong>Security & privacy</strong> → <strong>More security settings</strong> → <strong>Encryption & credentials</strong> → <strong>Install a certificate</strong> → <strong>CA certificate</strong>.</li>
|
||||
<li>Select the downloaded <code>wrolpi-ca.crt</code> file and confirm.</li>
|
||||
</ol>
|
||||
<p style="margin-top:0.5rem;font-size:0.9em;color:#999;">Menu paths vary by manufacturer. If you can't find it, search for <em>"certificate"</em> in Settings.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>macOS</summary>
|
||||
<div class="instructions">
|
||||
<ol>
|
||||
<li>Double-click the downloaded <code>wrolpi-ca.crt</code> file to open Keychain Access.</li>
|
||||
<li>Add it to the <strong>System</strong> or <strong>login</strong> keychain.</li>
|
||||
<li>Find <strong>WROLPi Root CA</strong> in the keychain, double-click it.</li>
|
||||
<li>Expand <strong>Trust</strong>, set <em>When using this certificate</em> to <strong>Always Trust</strong>.</li>
|
||||
<li>Close and enter your password to confirm.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Windows</summary>
|
||||
<div class="instructions">
|
||||
<ol>
|
||||
<li>Double-click the downloaded <code>wrolpi-ca.crt</code> file.</li>
|
||||
<li>Click <strong>Install Certificate</strong> → <strong>Local Machine</strong> → <strong>Next</strong>.</li>
|
||||
<li>Select <strong>Place all certificates in the following store</strong> → <strong>Browse</strong> → <strong>Trusted Root Certification Authorities</strong>.</li>
|
||||
<li>Click <strong>Finish</strong>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Linux (Debian / Ubuntu)</summary>
|
||||
<div class="instructions">
|
||||
<ol>
|
||||
<li>Copy the certificate (the <code>.crt</code> extension is required): <code>sudo cp wrolpi-ca.crt /usr/local/share/ca-certificates/</code></li>
|
||||
<li>Update the trust store: <code>sudo update-ca-certificates</code></li>
|
||||
<li>Restart your browser. Chromium-based browsers (Chrome, Edge) will use the system store automatically.</li>
|
||||
</ol>
|
||||
<p style="margin-top:0.5rem;font-size:0.9em;color:#999;">Firefox on Linux does <em>not</em> use the system trust store by default — see the Firefox section below.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Firefox (all platforms)</summary>
|
||||
<div class="instructions">
|
||||
<p style="margin-bottom:0.5rem;">Since Firefox 120, Firefox <strong>automatically trusts</strong> root certificates installed in the OS certificate store on <strong>Windows, macOS, and Android</strong>. If you've already installed the certificate above, Firefox should work with no extra steps on those platforms.</p>
|
||||
<p style="margin-bottom:0.5rem;">On <strong>Linux</strong> (or if auto-trust is disabled), import manually:</p>
|
||||
<ol>
|
||||
<li>Open Firefox, go to <strong>Settings</strong> → <strong>Privacy & Security</strong>.</li>
|
||||
<li>Scroll to <strong>Certificates</strong> → click <strong>View Certificates</strong>.</li>
|
||||
<li>In the <strong>Authorities</strong> tab, click <strong>Import</strong>.</li>
|
||||
<li>Select the downloaded <code>wrolpi-ca.crt</code> file.</li>
|
||||
<li>Check <strong>Trust this CA to identify websites</strong> → <strong>OK</strong>.</li>
|
||||
</ol>
|
||||
<p style="margin-top:0.5rem;font-size:0.9em;color:#999;">Alternatively, enable auto-trust: go to <code>about:config</code> and set <code>security.enterprise_roots.enabled</code> to <code>true</code>.</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="card" style="text-align:center;">
|
||||
<a class="btn btn-continue" id="continue-btn" href="#">Continue to WROLPi</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var host = window.location.hostname;
|
||||
var port = window.location.port;
|
||||
// Docker maps HTTP 8080 → HTTPS 8443; native uses default 443.
|
||||
var httpsPort = (port === '8080') ? ':8443' : '';
|
||||
var httpsBase = 'https://' + host + httpsPort;
|
||||
|
||||
document.getElementById('continue-btn').href = httpsBase;
|
||||
|
||||
// Auto-redirect if cert is already trusted.
|
||||
var controller = new AbortController();
|
||||
var timeout = setTimeout(function() { controller.abort(); }, 3000);
|
||||
fetch(httpsBase + '/api/echo', { signal: controller.signal })
|
||||
.then(function(r) {
|
||||
if (r.ok) {
|
||||
document.getElementById('auto-redirect').style.display = 'block';
|
||||
setTimeout(function() { window.location.href = httpsBase; }, 1500);
|
||||
}
|
||||
})
|
||||
.catch(function() {})
|
||||
.finally(function() { clearTimeout(timeout); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
user www-data;
|
||||
|
||||
events {
|
||||
|
||||
}
|
||||
|
||||
http {
|
||||
client_max_body_size 0;
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
charset utf-8;
|
||||
|
||||
include mime.types;
|
||||
types {
|
||||
# Custom types for ebooks.
|
||||
application/x-mobipocket-ebook mobi;
|
||||
application/epub+zip epub;
|
||||
# Configs
|
||||
text/yaml yaml yml;
|
||||
}
|
||||
|
||||
error_page 502 /error/50x.html;
|
||||
error_page 503 /error/50x.html;
|
||||
error_page 504 /error/50x.html;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _; # Default server for requests without "Host" header
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
proxy_pass_header Host;
|
||||
|
||||
# Self-signed cert.
|
||||
ssl_certificate /etc/nginx/cert.crt;
|
||||
ssl_certificate_key /etc/nginx/cert.key;
|
||||
|
||||
# CORS headers for Controller direct access (http://host:8087 -> https://host/api)
|
||||
# Added at server level so they apply to error pages (502, etc.)
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'Content-Type, Accept' always;
|
||||
|
||||
# Override http-level error_page to use named location with CORS
|
||||
error_page 502 503 504 @error_with_cors;
|
||||
|
||||
location @error_with_cors {
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'Content-Type, Accept' always;
|
||||
root /var/www;
|
||||
try_files /50x.html =502;
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/*;
|
||||
}
|
||||
|
||||
# HTTPS Map is served by Apache2. This is because mod_tile.conf is required to render the map. renderd does not
|
||||
# support nginx.
|
||||
|
||||
server {
|
||||
# HTTPS Kiwix.
|
||||
listen 8085 ssl;
|
||||
listen [::]:8085 ssl;
|
||||
proxy_pass_header Host;
|
||||
proxy_set_header 'Access-Control-Allow-Origin' '*';
|
||||
|
||||
ssl_certificate /etc/nginx/cert.crt;
|
||||
ssl_certificate_key /etc/nginx/cert.key;
|
||||
|
||||
location / {
|
||||
proxy_pass http://0.0.0.0:9085;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
# HTTPS Help.
|
||||
listen 8086 ssl;
|
||||
listen [::]:8086 ssl;
|
||||
proxy_pass_header Host;
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
|
||||
ssl_certificate /etc/nginx/cert.crt;
|
||||
ssl_certificate_key /etc/nginx/cert.key;
|
||||
|
||||
location / {
|
||||
proxy_pass http://0.0.0.0:9086;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
Listen 8084
|
||||
Listen 9084
|
||||
8
etc/raspberrypios/wrolpi-cert-renew.service
Normal file
8
etc/raspberrypios/wrolpi-cert-renew.service
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[Unit]
|
||||
Description=Renew WROLPi TLS certificates if expiring soon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/opt/wrolpi/scripts/renew_certificates.sh
|
||||
StandardOutput=journal
|
||||
10
etc/raspberrypios/wrolpi-cert-renew.timer
Normal file
10
etc/raspberrypios/wrolpi-cert-renew.timer
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[Unit]
|
||||
Description=Weekly check for WROLPi TLS certificate renewal
|
||||
|
||||
[Timer]
|
||||
OnCalendar=weekly
|
||||
Persistent=true
|
||||
RandomizedDelaySec=3600
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
# Nginx config for the WROLPi services.
|
||||
|
||||
#
|
||||
# Error pages
|
||||
#
|
||||
|
||||
location /error/ {
|
||||
alias /var/www/;
|
||||
internal;
|
||||
}
|
||||
|
||||
#
|
||||
# App
|
||||
#
|
||||
|
||||
location /favicon.ico {
|
||||
alias /opt/wrolpi/icon.ico;
|
||||
}
|
||||
|
||||
location /apple-touch-icon.png {
|
||||
alias /opt/wrolpi/icon.png;
|
||||
}
|
||||
|
||||
location / {
|
||||
# React APP with Controller fallback
|
||||
proxy_pass http://0.0.0.0:3000;
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 503 504 = @react_fallback;
|
||||
rewrite $1 break;
|
||||
}
|
||||
|
||||
location @react_fallback {
|
||||
return 302 /controller/?fallback=true;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://0.0.0.0:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
}
|
||||
|
||||
#
|
||||
# Controller - system management API and UI
|
||||
# All Controller endpoints are under /controller/*
|
||||
#
|
||||
location /controller {
|
||||
rewrite ^/controller(.*)$ $1 break;
|
||||
proxy_pass http://0.0.0.0:8087;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
#
|
||||
# Main API - handled by the API service (port 8081)
|
||||
#
|
||||
|
||||
location /api {
|
||||
proxy_pass http://0.0.0.0:8081;
|
||||
rewrite /^api(.*)$ $1 break;
|
||||
}
|
||||
|
||||
location /docs {
|
||||
proxy_pass http://0.0.0.0:8081;
|
||||
rewrite /^docs(.*)$ $1 break;
|
||||
}
|
||||
|
||||
location /media {
|
||||
# The media files mounted in the docker volume
|
||||
sendfile on;
|
||||
sendfile_max_chunk 1m;
|
||||
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
autoindex on;
|
||||
autoindex_exact_size off;
|
||||
alias /media/wrolpi;
|
||||
}
|
||||
|
||||
location /download {
|
||||
# Assume any request here is to download a file from the media directory.
|
||||
if ( $request_filename ~ '^.*/(.+)$' ) {
|
||||
# Always include the filename for downloads.
|
||||
add_header Content-Disposition 'attachment; filename="$1"';
|
||||
}
|
||||
|
||||
sendfile on;
|
||||
sendfile_max_chunk 1m;
|
||||
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
autoindex on;
|
||||
autoindex_exact_size off;
|
||||
alias /media/wrolpi;
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
[Unit]
|
||||
Description=WROLPi services group
|
||||
Wants=wrolpi-controller.service wrolpi-api.service wrolpi-app.service nginx.service postgresql.service wrolpi-kiwix.service renderd.service apache2.service postgresql@15-map.service
|
||||
Wants=wrolpi-controller.service wrolpi-api.service wrolpi-app.service caddy.service postgresql.service wrolpi-kiwix.service renderd.service apache2.service postgresql@15-map.service
|
||||
|
|
|
|||
69
help.sh
69
help.sh
|
|
@ -85,7 +85,7 @@ systemctl status postgresql.service >/dev/null &&
|
|||
echo "OK: Postgres found" ||
|
||||
echo "FAILED: Postgres service does not exist"
|
||||
|
||||
if netstat -ant | grep LISTEN | grep 127.0.0.1:5432 >/dev/null; then
|
||||
if netstat -ant | grep LISTEN | grep -E '[:\.]5432\b' >/dev/null; then
|
||||
echo "OK: Port 5432 is occupied"
|
||||
else
|
||||
echo "FAILED: Port 5432 is not occupied"
|
||||
|
|
@ -238,22 +238,59 @@ else
|
|||
echo "FAILED: WROLPi app did not respond with ebook interface"
|
||||
fi
|
||||
|
||||
if netstat -ant | grep LISTEN | grep 0.0.0.0:80 >/dev/null; then
|
||||
echo "OK: Port 80 is occupied"
|
||||
echo
|
||||
# Caddy
|
||||
|
||||
if command -v caddy &>/dev/null; then
|
||||
echo "OK: Caddy is installed ($(caddy version 2>/dev/null | head -1))"
|
||||
else
|
||||
echo "FAILED: Port 80 is not occupied"
|
||||
echo "FAILED: Caddy is not installed"
|
||||
fi
|
||||
|
||||
if systemctl list-unit-files "*nginx*" >/dev/null; then
|
||||
echo "OK: nginx systemd exists"
|
||||
if systemctl list-unit-files "*caddy*" >/dev/null 2>&1; then
|
||||
echo "OK: Caddy systemd exists"
|
||||
if systemctl status caddy >/dev/null 2>&1; then
|
||||
echo "OK: Caddy service is running"
|
||||
else
|
||||
echo "FAILED: Caddy service is not running"
|
||||
fi
|
||||
else
|
||||
echo "FAILED: nginx does not exist"
|
||||
echo "FAILED: Caddy systemd does not exist"
|
||||
fi
|
||||
|
||||
check_file /etc/caddy/Caddyfile "Caddyfile exists" "Caddyfile does not exist at /etc/caddy/Caddyfile"
|
||||
check_file /etc/ssl/wrolpi/cert.crt "TLS certificate exists" "TLS certificate does not exist at /etc/ssl/wrolpi/cert.crt"
|
||||
check_file /etc/ssl/wrolpi/cert.key "TLS key exists" "TLS key does not exist at /etc/ssl/wrolpi/cert.key"
|
||||
check_file ${MEDIA_DIRECTORY}/config/ssl/ca.crt "Root CA certificate exists" "Root CA certificate does not exist at ${MEDIA_DIRECTORY}/config/ssl/ca.crt"
|
||||
check_file ${MEDIA_DIRECTORY}/config/ssl/ca.key "Root CA key exists" "Root CA key does not exist at ${MEDIA_DIRECTORY}/config/ssl/ca.key"
|
||||
check_file /var/www/landing.html "Landing page exists" "Landing page does not exist at /var/www/landing.html"
|
||||
|
||||
if [ -f ${MEDIA_DIRECTORY}/config/ssl/ca.crt ] && [ -f /etc/ssl/wrolpi/cert.crt ]; then
|
||||
if openssl verify -CAfile ${MEDIA_DIRECTORY}/config/ssl/ca.crt /etc/ssl/wrolpi/cert.crt >/dev/null 2>&1; then
|
||||
echo "OK: Leaf certificate is signed by root CA"
|
||||
else
|
||||
echo "FAILED: Leaf certificate is NOT signed by root CA (regenerate with /opt/wrolpi/scripts/generate_certificates.sh)"
|
||||
fi
|
||||
fi
|
||||
|
||||
for port in 80 443 8084 8085 8086; do
|
||||
if netstat -ant | grep LISTEN | grep -E "(0\.0\.0\.0|:::)${port}\b" >/dev/null; then
|
||||
echo "OK: Caddy port ${port} is occupied"
|
||||
else
|
||||
echo "FAILED: Caddy port ${port} is not occupied"
|
||||
fi
|
||||
done
|
||||
|
||||
if curl -k -s https://0.0.0.0/ --max-time 5 | grep -i wrolpi >/dev/null; then
|
||||
echo "OK: Caddy HTTPS proxy responded on :443"
|
||||
else
|
||||
echo "FAILED: Caddy HTTPS proxy did not respond on :443"
|
||||
fi
|
||||
|
||||
echo
|
||||
# Map
|
||||
|
||||
if netstat -ant | grep LISTEN | grep 127.0.0.1:5433 >/dev/null; then
|
||||
if netstat -ant | grep LISTEN | grep -E '[:\.]5433\b' >/dev/null; then
|
||||
echo "OK: Port 5433 is occupied"
|
||||
else
|
||||
echo "FAILED: Port 5433 is not occupied"
|
||||
|
|
@ -314,10 +351,10 @@ else
|
|||
echo "FAILED: Media directory is not a mounted drive. (This is fine if you don't have an external drive.)"
|
||||
fi
|
||||
|
||||
if curl -k -s https://0.0.0.0/media/ --max-time 5 | grep "Index of /media/" >/dev/null; then
|
||||
echo "OK: Media directory files are served by nginx"
|
||||
if curl -k -s -I https://0.0.0.0/media/config/wrolpi.yaml --max-time 5 | grep '200 OK' >/dev/null; then
|
||||
echo "OK: Config can be fetched from nginx"
|
||||
if curl -k -s https://0.0.0.0/media/ --max-time 5 | grep -i "media" >/dev/null; then
|
||||
echo "OK: Media directory files are served by caddy"
|
||||
if curl -k -s -I https://0.0.0.0/media/config/wrolpi.yaml --max-time 5 | grep -E '200( OK)?' >/dev/null; then
|
||||
echo "OK: Config can be fetched from caddy"
|
||||
else
|
||||
echo "FAILED: Media directory files are not being served"
|
||||
fi
|
||||
|
|
@ -325,10 +362,10 @@ else
|
|||
echo "FAILED: Media directory files are not being served"
|
||||
fi
|
||||
|
||||
if curl -k -s https://0.0.0.0/download/ --max-time 5 | grep "Index of /download/" >/dev/null; then
|
||||
echo "OK: Media download directory files are served by nginx"
|
||||
if curl -k -s -I https://0.0.0.0/download/config/wrolpi.yaml --max-time 5 | grep '200 OK' >/dev/null; then
|
||||
echo "OK: Config can be downloaded from nginx"
|
||||
if curl -k -s -I https://0.0.0.0/download/ --max-time 5 | grep -E '200( OK)?' >/dev/null; then
|
||||
echo "OK: Media download directory files are served by caddy"
|
||||
if curl -k -s -I https://0.0.0.0/download/config/wrolpi.yaml --max-time 5 | grep -E '200( OK)?' >/dev/null; then
|
||||
echo "OK: Config can be downloaded from caddy"
|
||||
else
|
||||
echo "FAILED: Media download directory files are not being served"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -65,8 +65,6 @@ lua5.1
|
|||
mapnik-utils
|
||||
netcat-openbsd
|
||||
network-manager
|
||||
nginx-doc
|
||||
nginx-full
|
||||
nicstat
|
||||
nodejs
|
||||
npm
|
||||
|
|
|
|||
|
|
@ -59,14 +59,17 @@ echo "Controller pre-installed"
|
|||
|
||||
chown -R wrolpi:wrolpi /opt/wrolpi
|
||||
|
||||
# Configure nginx.
|
||||
cp /opt/wrolpi/etc/raspberrypios/nginx.conf /etc/nginx/nginx.conf
|
||||
# Install Caddy from official apt repo.
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
|
||||
apt-get update
|
||||
apt-get install -y caddy
|
||||
|
||||
# Configure Caddy.
|
||||
mkdir -p /etc/caddy
|
||||
cp /opt/wrolpi/etc/raspberrypios/Caddyfile /etc/caddy/Caddyfile
|
||||
cp /opt/wrolpi/etc/raspberrypios/50x.html /var/www/50x.html
|
||||
# Copy wrolpi.conf which defines the location blocks for reverse proxy.
|
||||
[ -f /etc/nginx/conf.d/default.conf ] && rm /etc/nginx/conf.d/default.conf
|
||||
cp /opt/wrolpi/etc/raspberrypios/wrolpi.conf /etc/nginx/conf.d/wrolpi.conf
|
||||
# Remove default site (WROLPi nginx.conf doesn't use sites-enabled).
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
cp /opt/wrolpi/etc/raspberrypios/landing.html /var/www/landing.html
|
||||
|
||||
# WROLPi needs a few privileged commands.
|
||||
cp /opt/wrolpi/etc/raspberrypios/90-wrolpi /etc/sudoers.d/90-wrolpi
|
||||
|
|
@ -78,11 +81,14 @@ mkdir /media/wrolpi
|
|||
chown wrolpi:wrolpi /media/wrolpi
|
||||
|
||||
cp /opt/wrolpi/etc/raspberrypios/wrolpi-*.service /etc/systemd/system/
|
||||
cp /opt/wrolpi/etc/raspberrypios/wrolpi-*.timer /etc/systemd/system/
|
||||
cp /opt/wrolpi/etc/raspberrypios/wrolpi.target /etc/systemd/system/
|
||||
# Enable first startup script.
|
||||
systemctl enable wrolpi-first-startup.service
|
||||
# Enable Controller service to start on boot.
|
||||
systemctl enable wrolpi-controller.service
|
||||
# Enable weekly certificate renewal check.
|
||||
systemctl enable wrolpi-cert-renew.timer
|
||||
|
||||
# Copy MOTD scripts, delete original.
|
||||
cp /opt/wrolpi/etc/raspberrypios/motd/20-wrolpi.motd /etc/update-motd.d/20-wrolpi
|
||||
|
|
|
|||
17
repair.sh
17
repair.sh
|
|
@ -22,7 +22,7 @@ systemctl stop wrolpi-controller.service || :
|
|||
systemctl stop wrolpi-api.service || :
|
||||
systemctl stop wrolpi-app.service || :
|
||||
systemctl stop wrolpi-kiwix.service || :
|
||||
systemctl stop nginx || :
|
||||
systemctl stop caddy || :
|
||||
systemctl stop renderd || :
|
||||
systemctl stop apache2 || :
|
||||
|
||||
|
|
@ -49,16 +49,16 @@ git reset HEAD --hard
|
|||
chown -R wrolpi:wrolpi /opt/wrolpi
|
||||
|
||||
# Copy configs to system.
|
||||
cp /opt/wrolpi/etc/raspberrypios/nginx.conf /etc/nginx/nginx.conf
|
||||
[ -f /etc/nginx/conf.d/default.conf ] && rm /etc/nginx/conf.d/default.conf
|
||||
cp /opt/wrolpi/etc/raspberrypios/wrolpi.conf /etc/nginx/conf.d/wrolpi.conf
|
||||
mkdir -p /etc/caddy
|
||||
cp /opt/wrolpi/etc/raspberrypios/Caddyfile /etc/caddy/Caddyfile
|
||||
cp /opt/wrolpi/etc/raspberrypios/50x.html /var/www/50x.html
|
||||
cp /opt/wrolpi/etc/raspberrypios/landing.html /var/www/landing.html
|
||||
|
||||
# Generate nginx certificate for HTTPS.
|
||||
# Generate certificate for HTTPS.
|
||||
/opt/wrolpi/scripts/generate_certificates.sh
|
||||
|
||||
# Start nginx quickly so user can access Controller from React UI
|
||||
systemctl start nginx || :
|
||||
# Start Caddy quickly so user can access Controller from React UI
|
||||
systemctl start caddy || :
|
||||
|
||||
# WROLPi needs a few privileged commands.
|
||||
cp /opt/wrolpi/etc/raspberrypios/90-wrolpi /etc/sudoers.d/90-wrolpi
|
||||
|
|
@ -68,12 +68,14 @@ visudo -c -f /etc/sudoers.d/90-wrolpi
|
|||
|
||||
# Install the systemd services
|
||||
cp /opt/wrolpi/etc/raspberrypios/wrolpi*.service /etc/systemd/system/
|
||||
cp /opt/wrolpi/etc/raspberrypios/wrolpi-*.timer /etc/systemd/system/
|
||||
cp /opt/wrolpi/etc/raspberrypios/wrolpi.target /etc/systemd/system/
|
||||
systemctl enable wrolpi-controller.service
|
||||
systemctl enable wrolpi-api.service
|
||||
systemctl enable wrolpi-app.service
|
||||
systemctl enable wrolpi-kiwix.service
|
||||
systemctl enable wrolpi-help.service
|
||||
systemctl enable wrolpi-cert-renew.timer
|
||||
|
||||
# Repair Controller (offline-safe - no pip install)
|
||||
repair_controller() {
|
||||
|
|
@ -128,7 +130,6 @@ cp /opt/wrolpi/etc/raspberrypios/index.html \
|
|||
/opt/wrolpi/modules/map/leaflet.js \
|
||||
/opt/wrolpi/modules/map/leaflet.css /var/www/html/
|
||||
chmod 644 /var/www/html/*
|
||||
a2enmod ssl
|
||||
a2enmod headers
|
||||
|
||||
systemctl enable renderd
|
||||
|
|
|
|||
|
|
@ -1,19 +1,94 @@
|
|||
#!/usr/bin/env bash
|
||||
# Generate self-signed SSL certificates for nginx if they don't exist.
|
||||
# Generate a root CA and signed leaf certificate for WROLPi.
|
||||
#
|
||||
# Root CA: stored in ${MEDIA_DIRECTORY}/config/ssl/ — persists forever.
|
||||
# Leaf cert: stored in ${CERT_DIR}/ — regenerated each run to pick up hostname/IP changes.
|
||||
#
|
||||
# Users trust the root CA once; leaf certs can be regenerated without re-trusting.
|
||||
|
||||
set -e
|
||||
|
||||
CERT_FILE="/etc/nginx/cert.crt"
|
||||
KEY_FILE="/etc/nginx/cert.key"
|
||||
MEDIA_DIRECTORY="${MEDIA_DIRECTORY:-/media/wrolpi}"
|
||||
CERT_DIR="${CERT_DIR:-/etc/ssl/wrolpi}"
|
||||
|
||||
if [[ -f "${CERT_FILE}" && -f "${KEY_FILE}" ]]; then
|
||||
echo "Certificates already exist at ${CERT_FILE} and ${KEY_FILE}"
|
||||
exit 0
|
||||
CA_DIR="${MEDIA_DIRECTORY}/config/ssl"
|
||||
CA_KEY="${CA_DIR}/ca.key"
|
||||
CA_CERT="${CA_DIR}/ca.crt"
|
||||
|
||||
LEAF_KEY="${CERT_DIR}/cert.key"
|
||||
LEAF_CERT="${CERT_DIR}/cert.crt"
|
||||
|
||||
mkdir -p "${CA_DIR}" "${CERT_DIR}"
|
||||
|
||||
# --- Root CA (only created once) ---
|
||||
if [[ -f "${CA_CERT}" && -f "${CA_KEY}" ]]; then
|
||||
echo "Root CA already exists at ${CA_CERT}"
|
||||
else
|
||||
echo "Generating WROLPi Root CA..."
|
||||
openssl genrsa -out "${CA_KEY}" 4096
|
||||
openssl req -new -x509 -nodes -key "${CA_KEY}" -out "${CA_CERT}" -days 36500 \
|
||||
-subj "/C=US/ST=State/L=City/O=WROLPi/CN=WROLPi Root CA" \
|
||||
-addext "basicConstraints = critical, CA:TRUE" \
|
||||
-addext "keyUsage = critical, keyCertSign, cRLSign" \
|
||||
-addext "nameConstraints = critical, permitted;DNS:.local, permitted;DNS:localhost, permitted;IP:127.0.0.0/255.0.0.0, permitted;IP:10.0.0.0/255.0.0.0, permitted;IP:172.16.0.0/255.240.0.0, permitted;IP:192.168.0.0/255.255.0.0, permitted;IP:::1/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff, permitted;IP:fc00::/fe00::, permitted;IP:fe80::/ffc0::"
|
||||
chmod 600 "${CA_KEY}"
|
||||
chmod 644 "${CA_CERT}"
|
||||
echo "Root CA generated."
|
||||
fi
|
||||
|
||||
echo "Generating nginx certificates..."
|
||||
openssl genrsa -out "${KEY_FILE}" 2048
|
||||
openssl req -new -x509 -nodes -key "${KEY_FILE}" -out "${CERT_FILE}" -days 3650 \
|
||||
-subj "/C=US/ST=State/L=City/O=Org/OU=WROLPi/CN=$(hostname).local"
|
||||
chmod 640 "${KEY_FILE}" "${CERT_FILE}"
|
||||
echo "Certificates generated successfully."
|
||||
# --- Leaf certificate (always regenerated) ---
|
||||
echo "Generating WROLPi leaf certificate..."
|
||||
|
||||
# Build SAN list dynamically.
|
||||
SAN="DNS:localhost,DNS:wrolpi.local"
|
||||
|
||||
HOSTNAME_SHORT="$(hostname)"
|
||||
if [[ -n "${HOSTNAME_SHORT}" ]]; then
|
||||
SAN="${SAN},DNS:${HOSTNAME_SHORT}.local"
|
||||
fi
|
||||
|
||||
SAN="${SAN},IP:127.0.0.1"
|
||||
|
||||
# Add all local IPs.
|
||||
for ip in $(hostname -I 2>/dev/null || true); do
|
||||
SAN="${SAN},IP:${ip}"
|
||||
done
|
||||
|
||||
# Append any extra SANs (e.g. host IP when running in Docker).
|
||||
if [[ -n "${EXTRA_SANS:-}" ]]; then
|
||||
SAN="${SAN},${EXTRA_SANS}"
|
||||
fi
|
||||
|
||||
echo "SANs: ${SAN}"
|
||||
|
||||
# Create CSR and sign with root CA.
|
||||
openssl genrsa -out "${LEAF_KEY}" 2048
|
||||
|
||||
openssl req -new -key "${LEAF_KEY}" \
|
||||
-subj "/C=US/ST=State/L=City/O=WROLPi/CN=${HOSTNAME_SHORT:-wrolpi}.local" \
|
||||
-out "${CERT_DIR}/cert.csr"
|
||||
|
||||
openssl x509 -req -in "${CERT_DIR}/cert.csr" \
|
||||
-CA "${CA_CERT}" -CAkey "${CA_KEY}" -CAcreateserial \
|
||||
-days 825 \
|
||||
-extfile <(printf "subjectAltName=%s\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth" "${SAN}") \
|
||||
-out "${LEAF_CERT}"
|
||||
|
||||
rm -f "${CERT_DIR}/cert.csr"
|
||||
chmod 640 "${LEAF_KEY}" "${LEAF_CERT}"
|
||||
|
||||
# Allow the caddy user to read the leaf cert/key (native installs).
|
||||
if id caddy >/dev/null 2>&1; then
|
||||
chown root:caddy "${LEAF_KEY}" "${LEAF_CERT}"
|
||||
fi
|
||||
|
||||
# Validate the chain.
|
||||
openssl verify -CAfile "${CA_CERT}" "${LEAF_CERT}"
|
||||
echo "Leaf certificate generated and verified."
|
||||
|
||||
# Install root CA into system trust store so the device trusts its own certs.
|
||||
if command -v update-ca-certificates >/dev/null 2>&1; then
|
||||
cp "${CA_CERT}" /usr/local/share/ca-certificates/wrolpi-ca.crt
|
||||
update-ca-certificates
|
||||
echo "Root CA installed in system trust store."
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -39,6 +39,14 @@ if [[ ! -f /opt/wrolpi-blobs/map-db-gis.dump || ! -s /opt/wrolpi-blobs/map-db-gi
|
|||
wget https://wrolpi.nyc3.cdn.digitaloceanspaces.com/map-db-gis.dump -O /opt/wrolpi-blobs/map-db-gis.dump
|
||||
fi
|
||||
|
||||
# Install Caddy from official apt repo if not already installed.
|
||||
if ! command -v caddy &>/dev/null; then
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
|
||||
apt-get update
|
||||
apt-get install -y caddy
|
||||
fi
|
||||
|
||||
# Install WROLPi Help.
|
||||
/opt/wrolpi/scripts/install_help_service.sh
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,14 @@ if [[ ! -f /opt/wrolpi-blobs/map-db-gis.dump || ! -s /opt/wrolpi-blobs/map-db-gi
|
|||
wget https://wrolpi.nyc3.cdn.digitaloceanspaces.com/map-db-gis.dump -O /opt/wrolpi-blobs/map-db-gis.dump
|
||||
fi
|
||||
|
||||
# Install Caddy from official apt repo if not already installed.
|
||||
if ! command -v caddy &>/dev/null; then
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
|
||||
apt-get update
|
||||
apt-get install -y caddy
|
||||
fi
|
||||
|
||||
# Install WROLPi Help.
|
||||
/opt/wrolpi/scripts/install_help_service.sh
|
||||
|
||||
|
|
|
|||
28
scripts/renew_certificates.sh
Normal file
28
scripts/renew_certificates.sh
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
# Check if the WROLPi leaf certificate is expired or expiring within 30 days.
|
||||
# If so, regenerate it and reload Caddy.
|
||||
|
||||
set -e
|
||||
|
||||
CERT_DIR="${CERT_DIR:-/etc/ssl/wrolpi}"
|
||||
LEAF_CERT="${CERT_DIR}/cert.crt"
|
||||
|
||||
if [ ! -f "${LEAF_CERT}" ]; then
|
||||
echo "Leaf certificate not found at ${LEAF_CERT}, generating..."
|
||||
/opt/wrolpi/scripts/generate_certificates.sh
|
||||
systemctl reload caddy || systemctl restart caddy || echo "Warning: failed to reload Caddy"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if cert expires within 30 days.
|
||||
if openssl x509 -checkend 2592000 -noout -in "${LEAF_CERT}" 2>/dev/null; then
|
||||
echo "Certificate is still valid for more than 30 days, no renewal needed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Certificate is expired or expiring within 30 days, renewing..."
|
||||
/opt/wrolpi/scripts/generate_certificates.sh
|
||||
|
||||
# Reload Caddy to pick up new cert.
|
||||
systemctl reload caddy || systemctl restart caddy || echo "Warning: failed to reload Caddy"
|
||||
echo "Certificate renewed and Caddy reloaded."
|
||||
|
|
@ -23,5 +23,5 @@ find /media/wrolpi/zims -iname '*.zim' -exec kiwix-manage ${LIBRARY} add {} \;
|
|||
|
||||
[ ! -f ${LIBRARY} ] && echo "Could not find any Zim files to import" && exit 2
|
||||
|
||||
# Serve HTTP kiwix on 9085. nginx will serve HTTPS on 8085.
|
||||
# Serve HTTP kiwix on 9085. Caddy will serve HTTPS on 8085.
|
||||
kiwix-serve --library ${LIBRARY} --address 0.0.0.0 --port 9085
|
||||
|
|
|
|||
|
|
@ -104,5 +104,18 @@ fi
|
|||
# Migrate map DB if necessary. Do this before repair because it will reset map if map db is empty.
|
||||
/opt/wrolpi/wrolpi/scripts/migrate_map_db.sh || echo "Map DB migration failed."
|
||||
|
||||
# Migrate from nginx to Caddy if necessary.
|
||||
if ! command -v caddy &>/dev/null; then
|
||||
echo "Installing Caddy (replacing nginx)..."
|
||||
# Disable and stop nginx if it was previously used.
|
||||
systemctl stop nginx 2>/dev/null || :
|
||||
systemctl disable nginx 2>/dev/null || :
|
||||
# Install Caddy from official apt repo.
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
|
||||
apt-get update
|
||||
apt-get install -y caddy
|
||||
fi
|
||||
|
||||
# Install any configs, restart services.
|
||||
/opt/wrolpi/repair.sh
|
||||
|
|
|
|||
|
|
@ -3,20 +3,22 @@
|
|||
|
||||
set -x
|
||||
|
||||
# Generate nginx certificate for HTTPS if it doesn't exist.
|
||||
# Generate certificate for HTTPS.
|
||||
/opt/wrolpi/scripts/generate_certificates.sh
|
||||
|
||||
# Ensure nginx config files are in place (safeguard in case build didn't complete this).
|
||||
if [ ! -f /etc/nginx/conf.d/wrolpi.conf ]; then
|
||||
echo "wrolpi.conf missing, copying from /opt/wrolpi..."
|
||||
cp /opt/wrolpi/etc/raspberrypios/wrolpi.conf /etc/nginx/conf.d/wrolpi.conf
|
||||
fi
|
||||
# Remove default site if it exists (WROLPi nginx.conf doesn't use sites-enabled).
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
# Copy landing page for HTTP certificate download.
|
||||
cp /opt/wrolpi/etc/raspberrypios/landing.html /var/www/landing.html
|
||||
|
||||
# Enable and start nginx now that certificates exist.
|
||||
systemctl enable nginx
|
||||
systemctl start nginx || echo "Warning: nginx failed to start"
|
||||
# Ensure Caddy config is in place (safeguard in case build didn't complete this).
|
||||
if [ ! -f /etc/caddy/Caddyfile ]; then
|
||||
echo "Caddyfile missing, copying from /opt/wrolpi..."
|
||||
mkdir -p /etc/caddy
|
||||
cp /opt/wrolpi/etc/raspberrypios/Caddyfile /etc/caddy/Caddyfile
|
||||
fi
|
||||
|
||||
# Enable and start Caddy now that certificates exist.
|
||||
systemctl enable caddy
|
||||
systemctl start caddy || echo "Warning: caddy failed to start"
|
||||
|
||||
# Copy skeleton files to first users home directory.
|
||||
USER_HOME=$(getent passwd 1000 | cut -d: -f6)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue