From 742c1ddce7c60e6aa37c618e3a4ee12c4ce0475a Mon Sep 17 00:00:00 2001 From: Igor Gaponenko Date: Fri, 28 Jun 2024 23:16:46 -0700 Subject: [PATCH] Extended the entry point for Czar HTTP frontend to support SSL/TLS config Switched to the SSL-based REST services for testing. --- admin/local/docker/compose/docker-compose.yml | 2 ++ admin/tools/docker/base/Dockerfile | 3 +- src/admin/etc/integration_tests.yaml | 2 +- .../python/lsst/qserv/admin/cli/entrypoint.py | 16 ++++++++- .../python/lsst/qserv/admin/cli/script.py | 33 +++++++++++++++++++ src/admin/python/lsst/qserv/admin/itest.py | 11 ++++--- src/czar/qserv-czar-http.cc | 6 +++- 7 files changed, 65 insertions(+), 8 deletions(-) diff --git a/admin/local/docker/compose/docker-compose.yml b/admin/local/docker/compose/docker-compose.yml index 1af25e667..a8c13e90d 100644 --- a/admin/local/docker/compose/docker-compose.yml +++ b/admin/local/docker/compose/docker-compose.yml @@ -399,6 +399,8 @@ services: --xrootd-manager czar_xrootd --http-frontend-port 4048 --http-frontend-threads 4 + --http-ssl-cert-file /config-etc/ssl/czar-cert.pem + --http-ssl-private-key-file /config-etc/ssl/czar-key.pem --log-cfg-file=/config-etc/log/log-czar-proxy.cnf --repl-instance-id qserv_proj --repl-auth-key replauthkey diff --git a/admin/tools/docker/base/Dockerfile b/admin/tools/docker/base/Dockerfile index c9ea08095..71bc86d55 100644 --- a/admin/tools/docker/base/Dockerfile +++ b/admin/tools/docker/base/Dockerfile @@ -232,9 +232,10 @@ WORKDIR /home/qserv RUN mkdir -p /qserv/data && \ mkdir /config-etc && \ + mkdir /config-etc/ssl && \ mkdir -p /qserv/run/tmp && \ mkdir -p /var/run/xrootd && \ - chown qserv:qserv /qserv/data /config-etc /qserv/run/tmp /var/run/xrootd + chown qserv:qserv /qserv/data /config-etc /config-etc/ssl /qserv/run/tmp /var/run/xrootd RUN alternatives --install /usr/bin/python python /usr/bin/python3.9 1 ENV PYTHONPATH "${PYTHONPATH}:/usr/local/python" diff --git a/src/admin/etc/integration_tests.yaml b/src/admin/etc/integration_tests.yaml index d28634082..64a4783c3 100644 --- a/src/admin/etc/integration_tests.yaml +++ b/src/admin/etc/integration_tests.yaml @@ -3,7 +3,7 @@ reference-db-uri: mysql://qsmaster@integration-test-reference:3306 reference-db-admin-uri: mysql://root:CHANGEME@integration-test-reference:3306 replication-controller-uri: repl://@repl_controller:25081 qserv-uri: qserv://qsmaster@czar_proxy:4040 -qserv-http-uri: http://czar_http:4048 +qserv-http-uri: https://czar_http:4048 czar-db-admin-uri: mysql://root:CHANGEME@czar_mariadb:3306 # The folder where the itest sources will be mounted in the container: qserv-testdata-dir: /tmp/qserv/itest_src diff --git a/src/admin/python/lsst/qserv/admin/cli/entrypoint.py b/src/admin/python/lsst/qserv/admin/cli/entrypoint.py index e679ee47c..34905ec26 100644 --- a/src/admin/python/lsst/qserv/admin/cli/entrypoint.py +++ b/src/admin/python/lsst/qserv/admin/cli/entrypoint.py @@ -569,6 +569,18 @@ def proxy(ctx: click.Context, **kwargs: Any) -> None: help="The number of threads for the HTTP server of the frontend. The value of http_frontend_threads is passed" " as a command-line parameter to the application." ) +@click.option( + "--http-ssl-cert-file", + help="A location of a file containing an SSL/TSL certificate.", + default="/config-etc/ssl/czar-cert.pem", + show_default=True, +) +@click.option( + "--http-ssl-private-key-file", + help="A location of a file containing an SSL/TSL private key.", + default="/config-etc/ssl/czar-key.pem", + show_default=True, +) @click.option( "--czar-cfg-file", help="Path to the czar config file.", @@ -601,8 +613,10 @@ def czar_http(ctx: click.Context, **kwargs: Any) -> None: db_uri=targs["db_uri"], czar_cfg_file=targs["czar_cfg_file"], czar_cfg_path=targs["czar_cfg_path"], - cmd=targs["cmd"], log_cfg_file=targs["log_cfg_file"], + http_ssl_cert_file=targs["http_ssl_cert_file"], + http_ssl_private_key_file=targs["http_ssl_private_key_file"], + cmd=targs["cmd"], ) diff --git a/src/admin/python/lsst/qserv/admin/cli/script.py b/src/admin/python/lsst/qserv/admin/cli/script.py index 1fb0a0a73..7dbbf3471 100644 --- a/src/admin/python/lsst/qserv/admin/cli/script.py +++ b/src/admin/python/lsst/qserv/admin/cli/script.py @@ -23,6 +23,7 @@ import logging import os import shlex +import socket import subprocess import sys import time @@ -657,6 +658,8 @@ def enter_czar_http( czar_cfg_file: str, czar_cfg_path: str, log_cfg_file: str, + http_ssl_cert_file : str, + http_ssl_private_key_file : str, cmd: str, ) -> None: """Entrypoint script for the proxy container. @@ -673,6 +676,10 @@ def enter_czar_http( Location to render the czar config file. log_cfg_file : `str` Location of the log4cxx config file. + http_ssl_cert_file : `str` + The path to the SSL certificate file. + http_ssl_private_key_file : `str` + The path to the SSL private key file. cmd : `str` The jinja2 template for the command for this function to execute. """ @@ -700,6 +707,32 @@ def enter_czar_http( _do_smig_block(qmeta_smig_dir, "qmeta", db_uri) + # check if the SSL certificate and private key files exist and create + # them if they don't. + if not os.path.exists(http_ssl_cert_file) or not os.path.exists(http_ssl_private_key_file): + _log.info("Generating self-signed SSL/TLS certificate %s and private key %s for HTTPS", + http_ssl_cert_file, http_ssl_private_key_file) + country = "US" + state = "California" + loc = "Menlo Park" + org = "SLAC National Accelerator Laboratory" + org_unit = "Rubin Observatory" + hostname = socket.gethostbyaddr(socket.gethostname())[0] # FQDN if available + subj = f"/C={country}/ST={state}/L={loc}/O={org}/OU={org_unit}/CN={hostname}" + openssl_cmd = [ + "openssl", "req", + "-x509", + "-newkey", "rsa:4096", + "-out", http_ssl_cert_file, + "-keyout", http_ssl_private_key_file, + "-sha256", + "-days", "365", + "-nodes", + "-subj", subj] + ret = subprocess.run(openssl_cmd, env=dict(os.environ,), cwd="/home/qserv") + if ret.returncode != 0: + raise RuntimeError("Failed to create SSL certificate and private key files.") + env = dict( os.environ, LD_PRELOAD=ld_preload, diff --git a/src/admin/python/lsst/qserv/admin/itest.py b/src/admin/python/lsst/qserv/admin/itest.py index 89ea7d200..9235cfd65 100644 --- a/src/admin/python/lsst/qserv/admin/itest.py +++ b/src/admin/python/lsst/qserv/admin/itest.py @@ -21,6 +21,7 @@ from filecmp import dircmp import json import logging +import urllib3 import requests import os import re @@ -510,7 +511,7 @@ def run_attached_http(self, connection: str, database: str) -> None: # Submit the query, check and analyze the completion status svc = str(urljoin(connection, '/query')) - req = requests.post(svc, json={'query': query, 'database': database, 'binary_encoding': 'hex'}) + req = requests.post(svc, json={'query': query, 'database': database, 'binary_encoding': 'hex'}, verify=False) req.raise_for_status() res = req.json() if res['success'] == 0: @@ -534,7 +535,7 @@ def run_detached_http(self, connection: str, database: str) -> None: # Submit the query via the async service, check and analyze the completion status svc = str(urljoin(connection, '/query-async')) - req = requests.post(svc, json={'query': query, 'database': database}) + req = requests.post(svc, json={'query': query, 'database': database}, verify=False) req.raise_for_status() res = req.json() if res['success'] == 0: @@ -547,7 +548,7 @@ def run_detached_http(self, connection: str, database: str) -> None: while time.time() < end_time: # Submit a request to check a status of the query svc = str(urljoin(connection, f"/query-async/status/{query_id}")) - req = requests.get(svc) + req = requests.get(svc, verify=False) req.raise_for_status() res = req.json() if res['success'] == 0: @@ -561,7 +562,7 @@ def run_detached_http(self, connection: str, database: str) -> None: # Make another request to pull the result set svc = str(urljoin(connection, f"/query-async/result/{query_id}?binary_encoding=hex")) - req = requests.get(svc) + req = requests.get(svc, verify=False) req.raise_for_status() res = req.json() if res['success'] == 0: @@ -747,6 +748,8 @@ def __init__( self.db_name, skip_numbers, ) + # Supress the warning about the self-signed certificate + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def run(self) -> None: """Run the test queries in a test case. diff --git a/src/czar/qserv-czar-http.cc b/src/czar/qserv-czar-http.cc index 48d46b037..880319972 100644 --- a/src/czar/qserv-czar-http.cc +++ b/src/czar/qserv-czar-http.cc @@ -51,7 +51,9 @@ int main(int argc, char* argv[]) { // - the port number (0 value would result in allocating the first available port) // - the number of service threads (0 value would assume the number of host machine's // hardware threads) - if (argc != 5) { + // - a location of the SSL/TSL certificate for the secure connections + // - a location of the SSL/TSL private key + if (argc != 7) { cerr << __func__ << ": insufficient number of the command-line parameters\n" << ::usage << endl; return 1; } @@ -73,6 +75,8 @@ int main(int argc, char* argv[]) { cerr << __func__ << ": failed to parse command line parameters\n" << ::usage << endl; return 1; } + string const sslCertFile = argv[nextArg++]; + string const sslPrivateKeyFile = argv[nextArg++]; try { auto const czar = czar::Czar::createCzar(configFilePath, czarName); auto const svc = czar::ChttpCzarSvc::create(port, numThreads, sslCertFile, sslPrivateKeyFile);