Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,18 @@
FROM nginx:latest

RUN rm -rf /usr/share/nginx/html/*

# htpasswd, used at container start to hash credentials supplied in the
# environment. See scripts/40-stack-auth.sh.
RUN apt-get update \
&& apt-get install -y --no-install-recommends apache2-utils \
&& rm -rf /var/lib/apt/lists/*

# Replaces the stock config, which serves the same content but has no place to
# put an authentication directive.
COPY nginx/default.conf /etc/nginx/conf.d/default.conf

# nginx's entrypoint runs this before starting the server.
COPY scripts/40-stack-auth.sh /docker-entrypoint.d/40-stack-auth.sh
RUN chmod +x /docker-entrypoint.d/40-stack-auth.sh \
&& mkdir -p /etc/nginx/stack-auth
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,50 @@ $ stack fetch repo bozemanpass/stack-wrapper-static-content
$ stack webapp build --wrapper static-content --source-repo ~/my-static-site
```

## Authentication

The content is served to anyone by default. Setting a username and password in the
environment puts it behind HTTP basic authentication instead:

```
$ docker run -p 3000:80 -e STACK_AUTH_USER=alice -e STACK_AUTH_PASSWORD=secret <image>
```

or, for a deployment made with the stack tool:

```
$ stack init --stack my-site --output spec.yml \
--config STACK_AUTH_USER=alice --config STACK_AUTH_PASSWORD=secret
```

| Variable | Meaning |
|----------|---------|
| `STACK_AUTH_USER`, `STACK_AUTH_PASSWORD` | One credential. The password is hashed when the container starts; the two must be set together or the container refuses to start. |
| `STACK_AUTH_HTPASSWD` | The content of an htpasswd file, already hashed — several users, without a plaintext password in the environment. Additive with the pair above. |
| `STACK_AUTH_REALM` | The name the browser's prompt shows. Defaults to `Restricted`. |
| `STACK_AUTH_EXCLUDE` | Space-separated path prefixes served without credentials. |

Two things worth knowing:

- **This is configurable after deployment.** The variables are read at container start,
not baked into the image, so a site that was deployed without authentication is gated by
adding them to the deployment's `config.env` and running `stack manage --dir <dir>
update`, which applies environment changes on every target. Removing them again ungates
it. The image is the same either way.
- **A composefile healthcheck needs `STACK_AUTH_EXCLUDE`.** On Kubernetes a healthcheck
becomes the container's liveness probe, and a probe answered with a 401 restarts the pod
for as long as authentication is configured. Point the healthcheck at a path named in
`STACK_AUTH_EXCLUDE`.

Basic authentication sends the password with every request, protected by nothing but
base64, so it is only worth having over HTTPS. See the stack tool's `docs/ingress.md` for
serving a deployment over TLS.

## Contents

- `wrapper.yml` — the wrapper manifest (see the stack tool's `docs/wrappers.md`)
- `Containerfile` — build for the `bozemanpass/static-content-base` base image
- `Containerfile.app` — wraps the app source into a servable image (build context is the app repository)
- `build.sh` — build script invoked by the stack tool
- `nginx/default.conf` — the served site's nginx configuration
- `scripts/40-stack-auth.sh` — run by nginx's entrypoint, configures authentication from the environment
23 changes: 23 additions & 0 deletions nginx/default.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
server {
listen 80;
server_name localhost;

# At the server level rather than in `location /` (where the stock config puts
# it) so that every location has it, including the ones written for
# STACK_AUTH_EXCLUDE, which would otherwise have no document root and answer
# 404 for content that is really there.
root /usr/share/nginx/html;

# Authentication, when it has been configured. The file this picks up is
# written at container start by /docker-entrypoint.d/40-stack-auth.sh from the
# STACK_AUTH_* environment; when no credentials are configured nothing is
# written, the glob matches nothing, and the content is served to anyone --
# which is what this image did before authentication existed.
include /etc/nginx/stack-auth/*.conf;

location / {
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
}
82 changes: 82 additions & 0 deletions scripts/40-stack-auth.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# Put the served content behind HTTP basic authentication, when the environment
# asks for it.
#
# Run by nginx's own entrypoint before the server starts, so the gate is a
# property of the image rather than of how the container was invoked: a wrapper
# contributes nothing to a deployment but an image, and the composefile that
# names the wrapped image belongs to whoever wrote the stack. Reading the
# environment at start is also what makes this configurable after the fact --
# adding the variables to a deployment's config.env and running `stack manage
# update` is a change to the container's environment, which is exactly what
# `update` applies.
#
# STACK_AUTH_USER, STACK_AUTH_PASSWORD one credential, hashed here
# STACK_AUTH_HTPASSWD htpasswd file content, already hashed
# STACK_AUTH_REALM the name the browser prompt shows
# STACK_AUTH_EXCLUDE paths to leave open, space separated
#
# With none of them set the content is served to anyone, as it always was.
set -e
if [ -n "$STACK_SCRIPT_DEBUG" ]; then
set -x
fi

auth_dir=/etc/nginx/stack-auth
conf_file=$auth_dir/auth.conf
htpasswd_file=$auth_dir/htpasswd

mkdir -p "$auth_dir"
# Written from scratch on every start. A container that is restarted rather than
# recreated still has what the last start wrote, and leaving that in place would
# go on demanding credentials the current environment no longer configures.
rm -f "$conf_file" "$htpasswd_file"

if [ -z "$STACK_AUTH_USER" ] && [ -z "$STACK_AUTH_PASSWORD" ] && [ -z "$STACK_AUTH_HTPASSWD" ]; then
echo "static-content: no authentication configured, the content is served to anyone."
echo "static-content: set STACK_AUTH_USER and STACK_AUTH_PASSWORD to require credentials."
exit 0
fi

# Half a credential is a misconfiguration either way, and both guesses are bad:
# taking the user alone serves the content unauthenticated, and taking the
# password alone locks it away behind a name nobody knows. Refuse to start.
if [ -n "$STACK_AUTH_USER" ] && [ -z "$STACK_AUTH_PASSWORD" ]; then
echo "static-content: STACK_AUTH_USER is set but STACK_AUTH_PASSWORD is not" >&2
exit 1
fi
if [ -n "$STACK_AUTH_PASSWORD" ] && [ -z "$STACK_AUTH_USER" ]; then
echo "static-content: STACK_AUTH_PASSWORD is set but STACK_AUTH_USER is not" >&2
exit 1
fi

touch "$htpasswd_file"
if [ -n "$STACK_AUTH_HTPASSWD" ]; then
printf '%s\n' "$STACK_AUTH_HTPASSWD" >> "$htpasswd_file"
fi
if [ -n "$STACK_AUTH_USER" ]; then
# Appending, so that a single credential and a supplied file are additive
# rather than one of them silently winning.
htpasswd -bB "$htpasswd_file" "$STACK_AUTH_USER" "$STACK_AUTH_PASSWORD" > /dev/null 2>&1
fi

# The workers read this file, and they do not run as root.
chown root:nginx "$htpasswd_file"
chmod 640 "$htpasswd_file"

{
echo "auth_basic \"${STACK_AUTH_REALM:-Restricted}\";"
echo "auth_basic_user_file ${htpasswd_file};"
# An excluded path is a prefix, and is served without credentials. A
# composefile healthcheck is the usual reason to want one: on Kubernetes it
# becomes the container's liveness probe, and a probe that is answered with a
# 401 restarts the pod for as long as authentication is configured.
for path in $STACK_AUTH_EXCLUDE; do
echo "location ${path} { auth_basic off; }"
done
} > "$conf_file"

echo "static-content: authentication enabled, $(grep -c . "$htpasswd_file") credential(s), realm \"${STACK_AUTH_REALM:-Restricted}\"."
for path in $STACK_AUTH_EXCLUDE; do
echo "static-content: ${path} is served without credentials."
done