SSL setup scripts & documentation (#67)

* Testing an nginx/SSL reverse proxy docker image

* Custom nginx and certbot containers

* Change certbot volume mounts

* Figuring some things out

* Challenges have to be run over HTTP?

* Docker networking

* remove a volume

* Added nginx & certbot to docker compose - working on droplet

* Use native nginx templating

* SSH please

* SSH setup scripts

* Updated .env and documentation for setting up HTTPS
n_workers
Peter Rauscher 2023-05-04 16:52:39 -04:00 committed by GitHub
parent 3622261f98
commit c2f2a237db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 176 additions and 35 deletions

View File

@ -8,3 +8,5 @@ POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD=postgrespw POSTGRES_PASSWORD=postgrespw
POSTGRES_SSLMODE=require POSTGRES_SSLMODE=require
CA_CERT=/usr/local/share/ca-certificates/ca-certificate.crt CA_CERT=/usr/local/share/ca-certificates/ca-certificate.crt
DOMAIN=oss.ebookfoundation.org
SSL_EMAIL=example@gmail.com

View File

@ -7,23 +7,23 @@ The OAPEN Suggestion Service uses natural-language processing to suggest books b
## Table of Contents ## Table of Contents
- [Installation (Server)](#installation-server) - [Installation (Server)](#installation-server)
* [DigitalOcean Droplet](#digitalocean-droplet) - [DigitalOcean Droplet](#digitalocean-droplet)
* [DigitalOcean Managed Database](#digitalocean-managed-database) - [DigitalOcean Managed Database](#digitalocean-managed-database)
* [Setup Users & Install Requirements](#setup-users-install-requirements) - [Setup Users & Install Requirements](#setup-users-install-requirements)
* [Clone & Configure the Project](#clone-configure-the-project) - [Clone & Configure the Project](#clone-configure-the-project)
* [SSL Certificate](#ssl-certificate) - [SSL Certificate](#ssl-certificate)
- [Running](#running) - [Running](#running)
- [Logging](#logging) - [Logging](#logging)
- [Endpoints](#endpoints) - [Endpoints](#endpoints)
* [/api](#get-api) - [/api](#get-api)
* [/api/ngrams](#get-apingrams) - [/api/ngrams](#get-apingrams)
* [/api/{handle}](#get-apihandle) - [/api/{handle}](#get-apihandle)
* [/api/{handle}/ngrams](#get-apihandlengrams) - [/api/{handle}/ngrams](#get-apihandlengrams)
- [Service Components](#service-components) - [Service Components](#service-components)
* [Suggestion Engine](#suggestion-engine) - [Suggestion Engine](#suggestion-engine)
* [API](#api) - [API](#api)
* [Embed Script](#embed-script) - [Embed Script](#embed-script)
* [Web Demo](#web-demo) - [Web Demo](#web-demo)
- [Updates](#updates) - [Updates](#updates)
- [Local Installation (No Server)](#local-installation-no-server) - [Local Installation (No Server)](#local-installation-no-server)
@ -134,15 +134,66 @@ The OAPEN Suggestion Service uses natural-language processing to suggest books b
POSTGRES_USERNAME=<Username of the postgres user> POSTGRES_USERNAME=<Username of the postgres user>
POSTGRES_PASSWORD=<Password of the postgres user> POSTGRES_PASSWORD=<Password of the postgres user>
POSTGRES_SSLMODE=<'require' when using a managed database> POSTGRES_SSLMODE=<'require' when using a managed database>
CA_CERT=<Path to the PostgreSQL ca-certificate.crt file>
DOMAIN=<domain the API should run on>
SSL_EMAIL=<email to send Certbot notifications to>
``` ```
> Postgres credentials can be found in the "Connection details" section of the managed database > Postgres credentials can be found in the "Connection details" section of the managed database
### SSL Certificate ### SSL Certificate
> Add information on how to retrieve certificate from DigitalOcean managed DB. #### SSL for Database
Create a directory in `api` called `certificates`. Once you have acquired a certificate for your managed database, copy it into `/api/certificates`. **Make sure that this file is named `ca-certificate.crt`, or ensure that the name of your certificate matches the `CA_CERT` variable in your `.env`.** > Add information on how to retrieve certificate from DigitalOcean managed DB.
Create a directory in `api` called `certificates`. Once you have acquired a certificate for your managed database, copy it into `/api/certificates`. **Make sure that this file is named `ca-certificate.crt`, or ensure that the name of your certificate matches the `CA_CERT` variable in your `.env`.**
#### SSL for API
To setup SSL for the API endpoint, you need to first ensure you have the proper ports open, both in DigitalOcean's built-in firewall, and on the droplet itself using `ufw`. DigitalOcean's firewall is sufficient, so if you like you can just `sudo ufw disable`.
If you'd like to keep both `ufw` and the DigitalOcean firewall running, enable the rules in `ufw`:
```bash
sudo ufw allow http
sudo ufw allow https
```
Next, enable ports `80` and `443` in the DigitalOcean dashboard for the droplet. `443` is for HTTPS traffic and `80` is for HTTP traffic, which is needed for certbot to re-issue certificates when they expire. Don't worry, nginx will redirect all non-certbot traffic to HTTPS automatically.
For certbot to issue an SSL certificate, your `DOMAIN` specified in `.env` must already have the proper DNS records pointing to the droplet's IPv4 address.
Then, just make sure the scripts are executeable:
```bash
chmod +x setup-ssh.sh ready-ssh.sh
```
And run them in this order.
```bash
./setup-ssh.sh
./ready-ssh.sh
```
> Wait for `setup-ssh.sh` to run to completion before running `ready-ssh.sh`.
The API should now be accessible by HTTPS only at `https://<domain>/api`!
However, to ensure that certificates are renewed before they expire, add a `cron` job that renews the certificate automatically. First, open the cron editor:
```bash
crontab -e
```
And add a line, replacing `/home/oapen/oapen-suggestion-service` with wherever you cloned the repository to locally:
```
0 5 1 */2 * /usr/bin/docker compose up -f /home/oapen/oapen-suggestion-service/docker-compose.yml certbot
```
Save your changes and exit. Now your certificates will renew automatically every 60 days!
## Running ## Running
@ -195,14 +246,11 @@ The array of books is ordered by the date they were added (most recent first).
Any combination of the query parameters in any order are valid. Any combination of the query parameters in any order are valid.
- `/api?threshold=3` - `/api?threshold=3`
Returns suggestions with a similarity score of 3 or more for the 25 most recently added books.
Returns suggestions with a similarity score of 3 or more for the 25 most recently added books.
- `/api?threshold=5&limit=100` - `/api?threshold=5&limit=100`
Returns suggestions with a similarity score of 3 or more for the 100 most recently added books.
Returns suggestions with a similarity score of 3 or more for the 100 most recently added books.
- `/api?limit=50&offset=1000` - `/api?limit=50&offset=1000`
Returns 50 books and all of their suggestions, skipping the 1000 most recent.
Returns 50 books and all of their suggestions, skipping the 1000 most recent.
### GET /api/ngrams ### GET /api/ngrams
@ -220,12 +268,9 @@ The array of books is ordered by the date they were added (most recent first).
Any combination of the query parameters in any order are valid. Any combination of the query parameters in any order are valid.
- `/api?limit=100` - `/api?limit=100`
Returns ngrams for the 100 most recent books.
Returns ngrams for the 100 most recent books.
- `/api?offset=1000` - `/api?offset=1000`
Returns ngrams for 25 books, skipping the 1000 most recent.
Returns ngrams for 25 books, skipping the 1000 most recent.
### GET /api/{handle} ### GET /api/{handle}
@ -236,6 +281,7 @@ Returns suggestions for the book with the specified handle.
`{handle}` (required): the handle of the book to retrieve. `{handle}` (required): the handle of the book to retrieve.
#### Query Parameters #### Query Parameters
`threshold` (optional): sets the minimum similarity score to receive suggestions for. Default is 0, returning all suggestions. `threshold` (optional): sets the minimum similarity score to receive suggestions for. Default is 0, returning all suggestions.
#### Examples #### Examples
@ -250,7 +296,6 @@ Returns suggestions for [the book](https://library.oapen.org/handle/20.500.12657
Returns suggestions with a similarity score of 3 or more for [the book](https://library.oapen.org/handle/20.500.12657/37041) with the handle `20.400.12657/47581`. Returns suggestions with a similarity score of 3 or more for [the book](https://library.oapen.org/handle/20.500.12657/37041) with the handle `20.400.12657/47581`.
### GET /api/{handle}/ngrams ### GET /api/{handle}/ngrams
Returns the ngrams and their occurences for the book with the specified handle. Returns the ngrams and their occurences for the book with the specified handle.

View File

@ -17,6 +17,6 @@ COPY ./certificates/* /usr/local/share/ca-certificates/
RUN chmod 644 /usr/local/share/ca-certificates/*.crt && update-ca-certificates RUN chmod 644 /usr/local/share/ca-certificates/*.crt && update-ca-certificates
EXPOSE 3001 EXPOSE ${API_PORT}
CMD [ "npm", "start" ] CMD [ "npm", "start" ]

View File

@ -1,4 +1,5 @@
const express = require("express"); const express = require("express");
const path = require("path");
const app = express(); const app = express();
const apiRoutes = require("./routes.js"); const apiRoutes = require("./routes.js");
@ -18,9 +19,9 @@ app.use("*", (req, res) => {
return res.status(404).json({ error: "Resource not found" }); return res.status(404).json({ error: "Resource not found" });
}); });
const port = process.env.API_PORT || 3001; const port = process.env.API_PORT || 8000;
app.listen(port, () => { app.listen(port, () => {
console.log("Suggestion Service API is up on port " + port); console.log("Suggestion Service API is up on port " + port);
console.log("Running at http://localhost:" + port + "/"); console.log("Running at http://localhost:" + port + "/api");
}); });

View File

@ -7,7 +7,7 @@ async function querySuggestions(handle, threshold = 0) {
await validate.checkHandle(handle); await validate.checkHandle(handle);
const query = new PQ({ const query = new PQ({
text: `SELECT suggestion AS handle, score text: `SELECT *
FROM oapen_suggestions.suggestions FROM oapen_suggestions.suggestions
WHERE handle = $1 WHERE handle = $1
AND score >= $2`, AND score >= $2`,

View File

@ -10,12 +10,37 @@ services:
- REFRESH_PERIOD=86400 # daily - REFRESH_PERIOD=86400 # daily
- HARVEST_PERIOD=604800 # weekly - HARVEST_PERIOD=604800 # weekly
api: api:
container_name: api
build: ./api/ build: ./api/
restart: always restart: always
env_file: env_file:
- .env - .env
ports: ports:
- "0.0.0.0:${API_PORT}:${API_PORT}" - 0.0.0.0:${API_PORT}:${API_PORT}
networks:
- nginx-passthrough
nginx:
image: nginx:mainline-alpine
restart: always
env_file:
- .env
volumes:
- ./nginx:/etc/nginx/templates
- /etc/certbot/conf:/etc/letsencrypt
- /etc/certbot/www:/var/www/certbot
ports:
- 80:80
- 443:443
networks:
- nginx-passthrough
certbot:
image: certbot/certbot
depends_on:
- nginx
volumes:
- /etc/certbot/conf:/etc/letsencrypt
- /etc/certbot/www:/var/www/certbot
command: certonly --webroot -w /var/www/certbot --force-renewal --email ${SSL_EMAIL} -d ${DOMAIN} --agree-tos
web: web:
build: ./web/ build: ./web/
restart: always restart: always
@ -26,6 +51,6 @@ services:
restart: always restart: always
ports: ports:
- "0.0.0.0:${EMBED_SCRIPT_PORT}:3002" - "0.0.0.0:${EMBED_SCRIPT_PORT}:3002"
volumes: networks:
db: nginx-passthrough:
driver: local driver: bridge

View File

@ -0,0 +1,12 @@
server {
listen 80;
server_name ${DOMAIN} www.${DOMAIN};
location ~ /.well-known/acme-challenge {
allow all;
root /var/www/certbot;
}
return 301 https://${DOMAIN}$request_uri;
}

23
nginx/nginx.conf Normal file
View File

@ -0,0 +1,23 @@
server {
listen 80;
server_name ${DOMAIN} www.${DOMAIN};
location ~ /.well-known/acme-challenge {
allow all;
root /var/www/certbot;
}
return 301 https://${DOMAIN}$request_uri;
}
server {
listen 443 ssl http2;
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
server_name ${DOMAIN} www.${DOMAIN};
location / {
proxy_pass http://api:${API_PORT}/;
}
}

23
nginx/nginx.conf.template Normal file
View File

@ -0,0 +1,23 @@
server {
listen 80;
server_name ${DOMAIN} www.${DOMAIN};
location ~ /.well-known/acme-challenge {
allow all;
root /var/www/certbot;
}
return 301 https://${DOMAIN}$request_uri;
}
server {
listen 443 ssl http2;
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
server_name ${DOMAIN} www.${DOMAIN};
location / {
proxy_pass http://api:${API_PORT}/;
}
}

5
ready-ssh.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
rm ./nginx/nginx.conf.template
cp ./nginx/nginx.conf ./nginx/nginx.conf.template
docker compose up --build -d nginx

5
setup-ssh.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
rm ./nginx/nginx.conf.template
cp ./nginx/nginx-challenge.conf ./nginx/nginx.conf.template
docker compose up --build -d nginx certbot