Skip to content

Commit

Permalink
fix integration tests; rework README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
zsteinkamp committed Mar 11, 2024
1 parent a9a012f commit 1283b3b
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 119 deletions.
25 changes: 1 addition & 24 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# syntax=docker/dockerfile:1
ARG NGINX_VERSION=1.25.3
ARG NJS_VERSION=0.8.2

FROM node:20-bullseye AS builder
FROM node:20 AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/app/.npm \
Expand All @@ -16,25 +15,3 @@ COPY --from=builder /app/dist/acme.js /usr/lib/nginx/njs_modules/acme.js
COPY ./examples/nginx.conf /etc/nginx/nginx.conf
RUN mkdir /etc/nginx/njs-acme
RUN chown nginx: /etc/nginx/njs-acme

# install the latest njs >= 0.8.1 (not yet bundled with nginx-1.25.2)
RUN --mount=type=cache,target=/var/cache/apt <<EOF
set -eux
export DEBIAN_FRONTEND=noninteractive
apt -qq update
apt install -qq --yes --no-install-recommends --no-install-suggests \
curl gnupg2 ca-certificates lsb-release debian-archive-keyring
update-ca-certificates
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
| tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
gpg --dry-run --quiet --no-keyring --import --import-options import-show \
/usr/share/keyrings/nginx-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/mainline/debian `lsb_release -cs` nginx" \
| tee /etc/apt/sources.list.d/nginx.list
apt update -qq
apt install -qq --yes --no-install-recommends --no-install-suggests \
nginx-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE}
apt remove --purge --auto-remove --yes
rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list
EOF
70 changes: 34 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@

# njs-acme

This repository provides a JavaScript library to work with [ACME](https://datatracker.ietf.org/doc/html/rfc8555) providers such as Let's Encrypt for [NJS](https://nginx.org/en/docs/njs/). The source code is compatible with the `ngx_http_js_module` runtime. This allows for the automatic issue of TLS/SSL certificates for NGINX.
This repository provides a JavaScript library to work with [ACME](https://datatracker.ietf.org/doc/html/rfc8555) providers such as Let's Encrypt for [NJS](https://nginx.org/en/docs/njs/). The source code is compatible with the `ngx_http_js_module` runtime. This allows for the automatic generation and renewal of TLS/SSL certificates for NGINX.

Requires at least `njs-0.8.1`, which is included with NGINX since nginx-1.26.**???-TBD**.
Requires at least `njs-0.8.2`, which is included with NGINX since `nginx-1.25.3`.

NOTE: Some ACME providers have strict rate limits. Please consult with your provider. For Let's Encrypt refer to their [rate-limits documentation](https://letsencrypt.org/docs/rate-limits/).

## Installation

There are a few options for using this repo. You can build a docker image to replace your existing NGINX image, use Docker to build the `acme.js` file, or build `acme.js` using a locally installed Node.js toolkit.
There are a few ways of using this repo. You can:
* build an ACME-enabled docker image to replace your existing NGINX image
* use Docker to build the `acme.js` file to use with your NGINX installation
* build `acme.js` using a locally installed Node.js toolkit to use with your NGINX installation

Each option above is detailed in each section below.

### Creating a Docker Image
To create an Nginx+NJS+njs-acme docker image, simply run:
```
> make docker-build
% make docker-build
...
=> exporting to image
=> => exporting layers
Expand All @@ -32,12 +37,12 @@ The image will be tagged `nginx/nginx-njs-acme`, where you can use it in place o

### Building `acme.js` With Docker

If you do not want to have to worry about installing Node.js and other build dependencies, then you can run this command:
If you want to use your own NGINX installation and do not want to have to worry about installing Node.js and other build dependencies, then you can run this command:
```
make docker-copy
```

This will build the full image and copy the `acme.js` file to the local `dist/` directory.
This will build the full image and copy the `acme.js` file to the local `dist/` directory. You can then include this file in your NGINX deployments.

### Building `acme.js` Without Docker

Expand All @@ -51,51 +56,59 @@ This will generate `dist/acme.js`, where you can then integrate it into your exi

## Configuration Variables

You can use environment variables or NGINX `js_var` directives to control the behavior of the NJS ACME client.
You can use environment variables _or_ NGINX `js_var` directives to control the behavior of the NJS ACME client.

In the case where both are defined, environment variables take precedence. Environment variables are in `ALL_CAPS`, whereas the nginx config variable is the same name, just prefixed with a dollar sign and `$lower_case`.

For example, `NJS_ACME_SERVER_NAMES` (env var) vs. `$njs_acme_server_names` (js_var).
For example, `NJS_ACME_SERVER_NAMES` (env var) is the same as `$njs_acme_server_names` (js_var).

### Required Variables

- `NJS_ACME_ACCOUNT_EMAIL`\
- `NJS_ACME_ACCOUNT_EMAIL` (env)\
`$njs_acme_account_email` (js_var)\
Your email address to send to the ACME provider.\
value: Any valid email address\
default: none (you must specify this!)

- `NJS_ACME_SERVER_NAMES`\
- `NJS_ACME_SERVER_NAMES` (env)\
`$njs_acme_server_names` (js_var)\
The hostname or list of hostnames to request the certificate for.\
value: Space-separated list of hostnames, e.g. `www1.mydomain.com www2.mydomain.com`\
default: none (you must specify this!)

### Optional Variables
- `NJS_ACME_VERIFY_PROVIDER_HTTPS`\
- `NJS_ACME_VERIFY_PROVIDER_HTTPS` (env)\
`$njs_acme_verify_provider_https` (js_var)\
Verify the ACME provider certificate when connecting.\
value: `false` | `true`\
default: `true`

- `NJS_ACME_DIRECTORY_URI`\
- `NJS_ACME_DIRECTORY_URI` (env)\
`$njs_acme_directory_uri` (js_var)\
ACME directory URL.\
value: Any valid URL\
default: `https://acme-staging-v02.api.letsencrypt.org/directory`

- `NJS_ACME_DIR`\
- `NJS_ACME_DIR` (env)\
`$njs_acme_dir` (js_var)\
Path to store ACME-related files such as keys, certificate requests, certificates, etc.\
value: Any valid system path writable by the `nginx` user. \
default: `/etc/nginx/njs-acme/`

- `NJS_ACME_CHALLENGE_DIR`\
- `NJS_ACME_CHALLENGE_DIR` (env)\
`$njs_acme_challenge_dir` (js_var)\
Path to store ACME-related challenge responses.\
value: Any valid system path writable by the `nginx` user. \
default: `${NJS_ACME_DIR}/challenge/`

- `NJS_ACME_ACCOUNT_PRIVATE_JWK`\
- `NJS_ACME_ACCOUNT_PRIVATE_JWK` (env)\
`$njs_acme_account_private_jwk` (js_var)\
Path to fetch/store the account private JWK.\
value: Path to the private JWK\
default: `${NJS_ACME_DIR}/account_private_key.json`

- `NJS_ACME_SHARED_DICT_ZONE_NAME`\
- `NJS_ACME_SHARED_DICT_ZONE_NAME` (env)\
`$njs_acme_shared_dict_zone_name` (js_var)\
[Shared Dictionary Zone](https://nginx.org/en/docs/http/ngx_http_js_module.html#js_shared_dict_zone) name .\
value: Zone name used as in `js_shared_dict_zone` directive\
default: `acme`
Expand All @@ -104,6 +117,8 @@ For example, `NJS_ACME_SERVER_NAMES` (env var) vs. `$njs_acme_server_names` (js_

There are a few pieces that are required to be present in your `nginx.conf` file. The file at [`examples/nginx.conf`](./examples/nginx.conf) shows them all.

The examples here use `js_var` for configuration variables, but keep in mind you can use the equivalent environment variables instead if that works better in your environment.

### Config Root
* Ensures the NJS module is loaded.
```nginx
Expand All @@ -115,7 +130,7 @@ There are a few pieces that are required to be present in your `nginx.conf` file
```nginx
js_path "/usr/lib/nginx/njs_modules/";
```
* Ensures the Let's Encrypt root certificate is loaded.
* Ensures a root certificate bundle is loaded into NJS.
```nginx
js_fetch_trusted_certificate /etc/ssl/certs/ISRG_Root_X1.pem;
```
Expand Down Expand Up @@ -158,7 +173,7 @@ This may also be defined with the environment variable `NJS_ACME_SERVER_NAMES`.
```

### `location` Blocks
* Location to handle ACME challenge requests.
* Location to handle ACME challenge requests. This must be accessible from the ACME server.
```nginx
location ~ "^/\.well-known/acme-challenge/[-_A-Za-z0-9]{22,128}$" {
js_content acme.challengeResponse;
Expand All @@ -172,21 +187,6 @@ request or renew certificates if necessary.
}
```

## Advanced
### Serving challenges directly from disk (not via njs)
If you do not wish to use `js_content acme.challengeResponse` to respond to challenge requests, then you can serve them directly with NGINX. Just be sure that the `root` directive value in your location block matches the value of `$njs_acme_challenge_dir`.

```nginx
server {
js_var $njs_acme_challenge_dir /etc/nginx/njs-acme/challenge;
<...>
location ~ "^/\.well-known/acme-challenge/[-_A-Za-z0-9]{22,128}$" {
default_type "text/plain";
root /etc/nginx/njs-acme/challenge/;
}
```

## Development

This project uses Babel and Rollup to compile TypeScript sources into a single JavaScript file for `njs`. It uses Mocha with nginx-testing for running integration tests against the NGINX server. This project uses [njs-typescript-starter](https://github.com/jirutka/njs-typescript-starter/tree/master) to write NJS modules and integration tests in TypeScript.
Expand Down Expand Up @@ -234,9 +234,7 @@ To follow these steps, you will need to have Node.js version 14.15 or greater in

docker compose logs -f nginx

3. When started initially, nginx would not have certificates at all (`$njs_acme_dir`, e.g. `/etc/nginx/njs-acme/`), so we can issue a new one by sending an HTTP request to a location with the `js_content` handler:

curl -vik --resolve proxy.nginx.com:8000:127.0.0.1 http://proxy.nginx.com:8000/acme/auto
3. When started initially, nginx will not have certificates at all. If you use the [example config](examples/nginx.conf), you will need to wait one minute for the `js_periodic` directive to invoke `acme.clientAutoMode` to create the certificate.

4. Send an HTTP request to nginx running in Docker:

Expand Down
27 changes: 1 addition & 26 deletions dev/Dockerfile.nginx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
ARG NGINX_VERSION=1.25.2
ARG NJS_VERSION=0.8.1
ARG NGINX_VERSION=1.25.3
FROM nginx:${NGINX_VERSION}
ARG NGINX_VERSION
ARG NJS_VERSION

RUN --mount=type=cache,target=/var/cache/apt <<EOF
set -eux
Expand All @@ -14,29 +12,6 @@ RUN --mount=type=cache,target=/var/cache/apt <<EOF
apt-get remove --purge --auto-remove --yes
EOF

RUN echo nginx-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE}

# As of 09/28/2023 NJS v0.8.1 is not included into nginx docker image.
# this a temprary patch to install it following installation steps from
# http://nginx.org/en/linux_packages.html#Debian
RUN --mount=type=cache,target=/var/cache/apt <<EOF
set -eux
export DEBIAN_FRONTEND=noninteractive
apt-get -qq update
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
| tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
gpg --dry-run --quiet --no-keyring --import --import-options import-show \
/usr/share/keyrings/nginx-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/mainline/debian $(echo $PKG_RELEASE | cut -f2 -d~) nginx" \
| tee /etc/apt/sources.list.d/nginx.list
apt-get -qq update
apt-get -qq install --yes --no-install-recommends --no-install-suggests \
nginx-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE}
apt-get remove --purge --auto-remove --yes
rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list
EOF

RUN mkdir -p /usr/lib/nginx/njs_modules
RUN mkdir -p /etc/nginx/njs-acme
RUN chown nginx: /etc/nginx/njs-acme
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ services:
- NJS_ACME_VERIFY_PROVIDER_HTTPS=false # only in development environment
- NJS_ACME_DIRECTORY_URI=https://pebble/dir # development server
ports:
- 8000:8000
- 4443:4443
- 8000:80
- 4443:443
networks:
default:
aliases:
Expand Down
4 changes: 2 additions & 2 deletions examples/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ http {
js_shared_dict_zone zone=acme:1m;

server {
listen 8000;
listen 4443 ssl;
listen 80;
listen 443 ssl;
server_name _default;

## Mandatory Variables
Expand Down
8 changes: 1 addition & 7 deletions integration-tests/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
# syntax=docker/dockerfile:1
FROM node:20-bullseye

ARG NGINX_VERSION=1.25.3
ARG NJS_VERSION=0.8.2

ENV NJS_ACME_DIR=/etc/nginx/njs-acme/

RUN env

# install nginx and njs
RUN --mount=type=cache,target=/var/cache/apt <<EOF
set -eux
Expand All @@ -25,8 +20,7 @@ RUN --mount=type=cache,target=/var/cache/apt <<EOF
| tee /etc/apt/sources.list.d/nginx.list
apt update -qq
apt install -qq --yes --no-install-recommends --no-install-suggests \
nginx=${NGINX_VERSION}-1~bullseye \
nginx-module-njs=${NGINX_VERSION}+${NJS_VERSION}-1~bullseye
nginx nginx-module-njs
apt remove --purge --auto-remove --yes
rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list
EOF
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/acme-auto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('Integration:AutoMode', async function () {
certInfo.certificate.domains.commonName.includes('Pebble Intermediate CA')
)
assert.equal(certInfo.certificate.domains.altNames.length, 1)
assert.equal(certInfo.certificate.domains.altNames[0][0], this.nginxHost)
assert.equal(certInfo.certificate.domains.altNames[0], this.nginxHost)

const httpsClient = this.client.extend({
prefixUrl: `https://${this.nginxHost}:${this.nginx.ports[1]}`,
Expand Down
5 changes: 3 additions & 2 deletions integration-tests/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
name: njs_acme_integration
services:
pebble:
# image: letsencrypt/pebble:latest
Expand Down Expand Up @@ -38,7 +39,7 @@ services:
- USE_NGINX_BIN_PATH=/usr/sbin/nginx
- NGINX_HOSTNAME=proxy.nginx.com
ports:
- 8000:8000
- 4443:4443
- 8000
- 4443
volumes:
certs:
12 changes: 8 additions & 4 deletions integration-tests/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ http {
# pebble usees 8000 as `httpPort` in dev/pebble/config.json so it can validate challebges
# and nginx must use the same
listen 0.0.0.0:__PORT__;
listen __PORT_1__ ssl http2;
listen __PORT_1__ ssl;
server_name __ADDRESS__;

js_var $njs_acme_server_names __ADDRESS__;
Expand All @@ -36,16 +36,20 @@ http {
ssl_certificate data:$dynamic_ssl_cert;
ssl_certificate_key data:$dynamic_ssl_key;

location = / {
return 200 '{"server_name":"$server_name", "ssl_session_id": "$ssl_session_id"}';
location = /health {
return 200 'OK';
}

location ~ "^/\.well-known/acme-challenge/[-_A-Za-z0-9]{22,128}$" {
js_content acme.challengeResponse;
}

location = /acme/auto {
js_content acme.clientAutoMode;
js_content acme.clientAutoModeWeb;
}

location = / {
return 200 '{"server_name":"$server_name","ssl_session_id":"$ssl_session_id"}';
}

}
Expand Down
Loading

0 comments on commit 1283b3b

Please sign in to comment.