Skip to content

Commit

Permalink
WIP Use certificate authentication in the ssl test
Browse files Browse the repository at this point in the history
This sets up nginx to accept client certificates and provides a matching
client certificate with DN=admin.
  • Loading branch information
mdellweg committed Sep 1, 2024
1 parent 139a30a commit e3171af
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 10 deletions.
99 changes: 99 additions & 0 deletions .ci/gen_certs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import argparse
import os
import sys
from datetime import datetime
from typing import List, Optional

import trustme

# ISO 8601
DATE_FORMAT = "%Y-%m-%d"


def main(argv: Optional[List[str]] = None) -> None:
if argv is None:
argv = sys.argv[1:]

parser = argparse.ArgumentParser(prog="trustme")
parser.add_argument(
"-d",
"--dir",
default=os.getcwd(),
help="Directory where certificates and keys are written to. Defaults to cwd.",
)
parser.add_argument(
"-i",
"--identities",
nargs="*",
default=("localhost", "127.0.0.1", "::1"),
help="Identities for the certificate. Defaults to 'localhost 127.0.0.1 ::1'.",
)
parser.add_argument(
"--common-name",
nargs=1,
default=None,
help="Also sets the deprecated 'commonName' field (only for the first identity passed).",
)
parser.add_argument(
"-x",
"--expires-on",
default=None,
help="Set the date the certificate will expire on (in YYYY-MM-DD format).",
metavar="YYYY-MM-DD",
)
parser.add_argument(
"-k",
"--key-type",
choices=list(t.name for t in trustme.KeyType),
default="ECDSA",
)

args = parser.parse_args(argv)
cert_dir = args.dir
identities = [str(identity) for identity in args.identities]
common_name = str(args.common_name[0]) if args.common_name else None
expires_on = (
None if args.expires_on is None else datetime.strptime(args.expires_on, DATE_FORMAT)
)
key_type = trustme.KeyType[args.key_type]

if not os.path.isdir(cert_dir):
raise ValueError(f"--dir={cert_dir} is not a directory")
if len(identities) < 1:
raise ValueError("Must include at least one identity")

# Generate the CA certificate
ca = trustme.CA(key_type=key_type)
# Write the certificate the client should trust
ca_cert_path = os.path.join(cert_dir, "ca.pem")
ca.cert_pem.write_to_path(path=ca_cert_path)

# Generate the server certificate
server_cert = ca.issue_cert(
*identities, common_name=common_name, not_after=expires_on, key_type=key_type
)
# Write the certificate and private key the server should use
server_key_path = os.path.join(cert_dir, "server.key")
server_cert_path = os.path.join(cert_dir, "server.pem")
server_cert.private_key_pem.write_to_path(path=server_key_path)
with open(server_cert_path, mode="w") as f:
f.truncate()
for blob in server_cert.cert_chain_pems:
blob.write_to_path(path=server_cert_path, append=True)

# Generate the client certificate
client_cert = ca.issue_cert(
"[email protected]", common_name="admin", not_after=expires_on, key_type=key_type
)
# Write the certificate and private key the client should use
client_key_path = os.path.join(cert_dir, "client.key")
client_cert_path = os.path.join(cert_dir, "client.pem")
client_cert.private_key_pem.write_to_path(path=client_key_path)
with open(client_cert_path, mode="w") as f:
f.truncate()
for blob in client_cert.cert_chain_pems:
blob.write_to_path(path=client_cert_path, append=True)


if __name__ == "__main__":
main()
147 changes: 147 additions & 0 deletions .ci/nginx.conf.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Copy from pulp-oci-images.
# Ideally we can get it upstream again.
#
# TODO: Support IPv6.
# TODO: Maybe serve multiple `location`s, not just one.

# The "nginx" package on fedora creates this user and group.
user nginx nginx;
# Gunicorn docs suggest this value.
worker_processes 1;
daemon off;
events {
worker_connections 1024; # increase if you have lots of clients
accept_mutex off; # set to 'on' if nginx worker_processes > 1
}

http {
include mime.types;
# fallback in case we can't determine a type
default_type application/octet-stream;
sendfile on;

# If left at the default of 1024, nginx emits a warning about being unable
# to build optimal hash types.
types_hash_max_size 4096;

map $ssl_client_s_dn $ssl_client_s_dn_cn {
default "";
~CN=(?<CN>[^,]+) $CN;
}

upstream pulp-content {
server 127.0.0.1:24816;
}

upstream pulp-api {
server 127.0.0.1:24817;
}

server {
# Gunicorn docs suggest the use of the "deferred" directive on Linux.
{% if https | default(false) -%}
listen 443 default_server deferred ssl;

ssl_certificate /etc/pulp/certs/pulp_webserver.crt;
ssl_certificate_key /etc/pulp/certs/pulp_webserver.key;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;

# intermediate configuration
ssl_protocols TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_prefer_server_ciphers on;

# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
add_header Strict-Transport-Security max-age=15768000;

# Configure client cert authentication
ssl_client_certificate /etc/pulp/certs/ca.pem;
ssl_verify_client optional;
{%- else -%}
listen 80 default_server deferred;
{%- endif %}
server_name $hostname;

# The default client_max_body_size is 1m. Clients uploading
# files larger than this will need to chunk said files.
client_max_body_size 10m;

# Gunicorn docs suggest this value.
keepalive_timeout 5;

location {{ content_path }} {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://pulp-content;
}

location {{ api_root }}api/v3/ {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header Remoteuser $ssl_client_s_dn_cn;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://pulp-api;
client_max_body_size 0;
}

{%- if domain_enabled | default(false) %}
location ~ {{ api_root }}.+/api/v3/ {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://pulp-api;
client_max_body_size 0;
}
{%- endif %}

location /auth/login/ {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://pulp-api;
}

include pulp/*.conf;

location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://pulp-api;
# most pulp static files are served through whitenoise
# http://whitenoise.evans.io/en/stable/
}

{%- if https | default(false) %}
# ACME http-01 tokens, i.e, for Let's Encrypt
location /.well-known/ {
try_files $uri $uri/ =404;
}
{%- endif %}
}
{%- if https | default(false) %}
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
{%- endif %}
}
28 changes: 18 additions & 10 deletions .ci/run_container.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ then
fi
export CONTAINER_RUNTIME

TMPDIR="$(mktemp -d)"
PULP_CLI_TEST_TMPDIR="$(mktemp -d)"
export PULP_CLI_TEST_TMPDIR

cleanup () {
"${CONTAINER_RUNTIME}" stop pulp-ephemeral && true
rm -rf "${TMPDIR}"
rm -rf "${PULP_CLI_TEST_TMPDIR}"
}

trap cleanup EXIT
Expand Down Expand Up @@ -48,8 +49,8 @@ else
SELINUX=""
fi;

mkdir -p "${TMPDIR}/settings/certs"
cp "${BASEPATH}/settings/settings.py" "${TMPDIR}/settings"
mkdir -p "${PULP_CLI_TEST_TMPDIR}/settings/certs"
cp "${BASEPATH}/settings/settings.py" "${PULP_CLI_TEST_TMPDIR}/settings"

if [ -z "${PULP_HTTPS:+x}" ]
then
Expand All @@ -60,10 +61,16 @@ else
PROTOCOL="https"
PORT="443"
PULP_CONTENT_ORIGIN="https://localhost:8080/"
python3 -m trustme -d "${TMPDIR}/settings/certs"
export PULP_CA_BUNDLE="${TMPDIR}/settings/certs/client.pem"
ln -fs server.pem "${TMPDIR}/settings/certs/pulp_webserver.crt"
ln -fs server.key "${TMPDIR}/settings/certs/pulp_webserver.key"
python3 "${BASEPATH}/gen_certs.py" -d "${PULP_CLI_TEST_TMPDIR}/settings/certs"
export PULP_CA_BUNDLE="${PULP_CLI_TEST_TMPDIR}/settings/certs/ca.pem"
ln -fs server.pem "${PULP_CLI_TEST_TMPDIR}/settings/certs/pulp_webserver.crt"
ln -fs server.key "${PULP_CLI_TEST_TMPDIR}/settings/certs/pulp_webserver.key"
{
echo "AUTHENTICATION_BACKENDS = '@json [\"django.contrib.auth.backends.RemoteUserBackend\"]'"
echo "MIDDLEWARE = '@merge django.contrib.auth.middleware.RemoteUserMiddleware'"
echo "REST_FRAMEWORK__DEFAULT_AUTHENTICATION_CLASSES = '@merge pulpcore.app.authentication.PulpRemoteUserAuthentication'"
echo "REMOTE_USER_ENVIRON_NAME = 'HTTP_REMOTEUSER'"
} >> "${PULP_CLI_TEST_TMPDIR}/settings/settings.py"
fi
export PULP_CONTENT_ORIGIN

Expand All @@ -75,7 +82,8 @@ export PULP_CONTENT_ORIGIN
--env PULP_CONTENT_ORIGIN \
--detach \
--name "pulp-ephemeral" \
--volume "${TMPDIR}/settings:/etc/pulp${SELINUX:+:Z}" \
--volume "${PULP_CLI_TEST_TMPDIR}/settings:/etc/pulp${SELINUX:+:Z}" \
--volume "${BASEPATH}/nginx.conf.j2:/nginx/nginx.conf.j2${SELINUX:+:Z}" \
--network bridge \
--publish "8080:${PORT}" \
"ghcr.io/pulp/pulp:${IMAGE_TAG}"
Expand Down Expand Up @@ -105,7 +113,7 @@ done
"${CONTAINER_RUNTIME}" exec "pulp-ephemeral" pulpcore-manager reset-admin-password --password password

# Create pulp config
PULP_CLI_CONFIG="${TMPDIR}/settings/certs/cli.toml"
PULP_CLI_CONFIG="${PULP_CLI_TEST_TMPDIR}/settings/certs/cli.toml"
export PULP_CLI_CONFIG
pulp config create --overwrite --location "${PULP_CLI_CONFIG}" --base-url "${PROTOCOL}://localhost:8080" ${PULP_API_ROOT:+--api-root "${PULP_API_ROOT}"} --username "admin" --password "password"
# show pulpcore/plugin versions we're using
Expand Down
6 changes: 6 additions & 0 deletions pytest_pulp_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,16 @@ def pulp_cli_settings() -> t.Dict[str, t.Dict[str, t.Any]]:
It is most likely not useful to be included standalone.
The `pulp_cli_env` fixture, however depends on it and sets $XDG_CONFIG_HOME up accordingly.
"""
pulp_cli_test_tmpdir = pathlib.Path(os.environ.get("PULP_CLI_TEST_TMPDIR", "."))
settings = toml.load(os.environ.get("PULP_CLI_CONFIG", "tests/cli.toml"))
if os.environ.get("PULP_HTTPS"):
for key in settings:
settings[key]["base_url"] = settings[key]["base_url"].replace("http://", "https://")
settings[key].pop("username", None)
settings[key].pop("password", None)
settings[key]["cert"] = str(pulp_cli_test_tmpdir / "settings" / "certs" / "client.pem")
settings[key]["key"] = str(pulp_cli_test_tmpdir / "settings" / "certs" / "client.key")

if os.environ.get("PULP_API_ROOT"):
for key in settings:
settings[key]["api_root"] = os.environ["PULP_API_ROOT"]
Expand Down

0 comments on commit e3171af

Please sign in to comment.