From ab3707870430cc1842f78718e35225ba753feae0 Mon Sep 17 00:00:00 2001 From: William Desportes Date: Sat, 11 Jan 2025 00:35:04 +0100 Subject: [PATCH] Test the SSL connection (#441) --- .github/workflows/run-tests.yml | 2 +- Makefile | 2 +- testing/conftest.py | 5 + .../docker-compose.testing-one-ssl-host.yml | 105 ++++++++++++++++++ testing/generate-keys.sh | 37 ++++++ testing/phpmyadmin_test.py | 38 ++++++- testing/secure-user.sql | 0 testing/test-docker.sh | 63 ++++++++++- 8 files changed, 244 insertions(+), 8 deletions(-) create mode 100644 testing/docker-compose/docker-compose.testing-one-ssl-host.yml create mode 100755 testing/generate-keys.sh create mode 100644 testing/secure-user.sql diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 5341de0..b8a7d4f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: database-image: ["mariadb:10.6", "mariadb:10.11", "mariadb:latest", "mysql:5.7", "mysql:latest"] - configuration: ["default", "one-host", "one-socket-host", "config-mount-dir", "fs-import-export", "different-apache-port", "run-as-www-data"] + configuration: ["default", "one-host", "one-socket-host", "config-mount-dir", "fs-import-export", "different-apache-port", "run-as-www-data", "one-ssl-host"] steps: - uses: actions/checkout@v4 diff --git a/Makefile b/Makefile index 8d6db88..ac74364 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ testing-%: docker compose -p "phpmyadmin_$@" -f ./testing/docker-compose/docker-compose.$@.yml up --build --abort-on-container-exit --exit-code-from=sut docker compose -p "phpmyadmin_$@" -f ./testing/docker-compose/docker-compose.$@.yml down -run-tests: testing-default testing-one-host testing-one-socket-host testing-config-mount-dir testing-fs-import-export testing-different-apache-port testing-run-as-www-data +run-tests: testing-default testing-one-host testing-one-socket-host testing-config-mount-dir testing-fs-import-export testing-different-apache-port testing-run-as-www-data testing-one-ssl-host logs: docker compose -f ./testing/docker-compose/docker-compose.testing-default.yml logs diff --git a/testing/conftest.py b/testing/conftest.py index 8631ea2..8041a1d 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -4,6 +4,7 @@ def pytest_addoption(parser): parser.addoption("--url") parser.addoption("--username") parser.addoption("--password") + parser.addoption("--root-password") parser.addoption("--server") parser.addoption("--sqlfile") @@ -19,6 +20,10 @@ def username(request): def password(request): return request.config.getoption("--password") +@pytest.fixture +def root_password(request): + return request.config.getoption("--root-password") + @pytest.fixture def server(request): return request.config.getoption("--server") diff --git a/testing/docker-compose/docker-compose.testing-one-ssl-host.yml b/testing/docker-compose/docker-compose.testing-one-ssl-host.yml new file mode 100644 index 0000000..2618f97 --- /dev/null +++ b/testing/docker-compose/docker-compose.testing-one-ssl-host.yml @@ -0,0 +1,105 @@ +version: "3.1" + +services: + db_server: + image: ${DB:-mariadb:11} + command: + - "--plugin_load_add=server_audit" + - "--server_audit=FORCE_PLUS_PERMANENT" + - "--server_audit_events=connect" + - "--server_audit_logging" + #- "--server_audit_file_path=/var/log/mariadb-audit/audit.log" + - "--ssl-ca=/etc/phpmyadmin/ssl/ca-cert.pem" + - "--ssl-cert=/etc/phpmyadmin/ssl/server-cert.pem" + - "--ssl-key=/etc/phpmyadmin/ssl/server-key.pem" + - "--require-secure-transport=ON" + - "--server-audit-logging=ON" + environment: + MARIADB_USER: secure-user + MARIADB_PASSWORD: "${TESTSUITE_PASSWORD:-my-secret-pw}" + MARIADB_ROOT_PASSWORD: "${TESTSUITE_ROOT_PASSWORD:-random-pass}" + # The database name used in the import test + MARIADB_DATABASE: World + healthcheck: + test: ["CMD", "mariadb-admin", "ping", "-uroot", "-prandom-pass"] + start_period: 10s + interval: 5s + timeout: 60s + retries: 10 + networks: + testing: + aliases: + - mariadb.phpmyadmin.local + tmpfs: + - /var/lib/mysql:rw,noexec,nosuid,size=300m + volumes: + #- ../secure-user.sql:/docker-entrypoint-initdb.d/secure-user.sql:ro + - ../ca-cert.pem:/etc/phpmyadmin/ssl/ca-cert.pem:ro + - ../ca-key.pem:/etc/phpmyadmin/ssl/ca-key.pem:ro + - ../server-cert.pem:/etc/phpmyadmin/ssl/server-cert.pem:ro + - ../server-key.pem:/etc/phpmyadmin/ssl/server-key.pem:ro + #- ../mariadb-audit:/var/log/mariadb-audit + + phpmyadmin: + build: + context: ../../apache + environment: + PMA_HOST: mariadb.phpmyadmin.local + PMA_SSL: 1 + PMA_SSL_VERIFY: 1 + PMA_SSL_CA: /etc/phpmyadmin/ssl/ca-cert.pem + PMA_SSL_CERT: /etc/phpmyadmin/ssl/client-cert.pem + PMA_SSL_KEY: /etc/phpmyadmin/ssl/client-key.pem + UPLOAD_LIMIT: 123M + MAX_EXECUTION_TIME: 125 + HIDE_PHP_VERSION: 1 + volumes: + - ../config.user.inc.php:/etc/phpmyadmin/config.user.inc.php:ro + - ../ca-cert.pem:/etc/phpmyadmin/ssl/ca-cert.pem:ro + - ../client-cert.pem:/etc/phpmyadmin/ssl/client-cert.pem:ro + - ../client-key.pem:/etc/phpmyadmin/ssl/client-key.pem:ro + healthcheck: + test: ["CMD", "curl", "-Ss", "http://localhost/robots.txt"] + start_period: 5s + interval: 3s + timeout: 60s + retries: 10 + networks: + testing: + aliases: + - phpmyadmin_testing_apache + depends_on: + db_server: + condition: service_healthy + + sut: + depends_on: + phpmyadmin: + condition: service_healthy + db_server: + condition: service_healthy + build: + context: ../ + command: "/tests/testing/test-docker.sh" + networks: + testing: + environment: + TESTSUITE_HOSTNAME: phpmyadmin_testing_apache + TESTSUITE_PORT: 80 + TESTSUITE_USER: secure-user + TESTSUITE_PASSWORD: "${TESTSUITE_PASSWORD:-my-secret-pw}" + TESTSUITE_ROOT_PASSWORD: "${TESTSUITE_ROOT_PASSWORD:-random-pass}" + PMA_HOST: mariadb.phpmyadmin.local + PMA_PORT: 3306 + IS_USING_SSL: true + volumes: + - ../ca-cert.pem:/etc/phpmyadmin/ssl/ca-cert.pem:ro + - ../server-cert.pem:/etc/phpmyadmin/ssl/server-cert.pem:ro + - ../client-cert.pem:/etc/phpmyadmin/ssl/client-cert.pem:ro + - ../client-key.pem:/etc/phpmyadmin/ssl/client-key.pem:ro + - ../../:/tests:ro + working_dir: /tests + +networks: + testing: + driver: bridge diff --git a/testing/generate-keys.sh b/testing/generate-keys.sh new file mode 100755 index 0000000..74c14de --- /dev/null +++ b/testing/generate-keys.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +set -eu + +# Source: https://github.com/chio-nzgft/docker-MariaDB-with-SSL +# See: https://dev.mysql.com/doc/refman/5.7/en/creating-ssl-files-using-openssl.html + + +ROOT_DIR="$(realpath $(dirname $0))" +echo "Using root dir: $ROOT_DIR" + +cd "$ROOT_DIR" + +rm -f *.pem + +SUBJECT_CA="/C=US/O=phpMyAdmin testing/OU=Docker/CN=ssl-ca.phpmyadmin.local/emailAddress=ssl-ca@example.org" +SUBJECT_CLIENT="/C=US/O=phpMyAdmin testing/OU=Docker/CN=client.phpmyadmin.local/emailAddress=secure-user@example.org" +SUBJECT_SERVER="/C=US/O=phpMyAdmin testing/OU=Docker/CN=mariadb.phpmyadmin.local" + +echo "CA key" + +openssl genrsa 2048 > ca-key.pem +openssl req -new -x509 -nodes -days 3600 -subj "${SUBJECT_CA}" -key ca-key.pem -out ca-cert.pem +echo "server key" + +openssl req -subj "${SUBJECT_SERVER}" -newkey rsa:2048 -days 3600 -nodes -keyout server-key.pem -out server-req.pem +openssl rsa -in server-key.pem -out server-key.pem +openssl x509 -req -in server-req.pem -days 3600 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out server-cert.pem +echo "client key" + +openssl req -subj "${SUBJECT_CLIENT}" -newkey rsa:2048 -days 3600 -nodes -keyout client-key.pem -out client-req.pem +openssl rsa -in client-key.pem -out client-key.pem +openssl x509 -req -in client-req.pem -days 3600 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out client-cert.pem +echo "check key ok" + +openssl verify -CAfile ca-cert.pem server-cert.pem client-cert.pem +chmod 666 *.pem diff --git a/testing/phpmyadmin_test.py b/testing/phpmyadmin_test.py index 5bd24b1..298f90d 100755 --- a/testing/phpmyadmin_test.py +++ b/testing/phpmyadmin_test.py @@ -58,7 +58,9 @@ def test_import(url, username, password, server, sqlfile): br.select_form('import') br.form.add_file(open(sqlfile), 'text/plain', sqlfile) response = br.submit() - assert(b'5326 queries executed' in response.read()) + response = response.read() + + assert(b'5326 queries executed' in response) def docker_secret(env_name): @@ -89,14 +91,46 @@ def test_phpmyadmin_secrets(): docker_secret('PMA_CONTROLUSER') docker_secret('PMA_CONTROLPASS') +def test_is_using_ssl(url, username, password, server): + is_using_ssl = os.environ.get('IS_USING_SSL'); + + br = create_browser() + response = do_login(br, url, username, password, server) + response = response.read() + + assert(b'Server connection' in response) + + if is_using_ssl: + assert(b'SSL is used' in response) + assert(b'SSL is not being used' not in response) + else: + assert(b'SSL is used' not in response) + assert(b'SSL is not being used' in response) + +def test_is_using_ssl_client_cert(url, server): + is_using_ssl = os.environ.get('IS_USING_SSL'); + if not is_using_ssl: + pytest.skip("Missing IS_USING_SSL ENV", allow_module_level=True) + + br = create_browser() + password = "" + response = do_login(br, url, "ssl-specific-user", password, server) + response = response.read() + + assert(b'Server connection' in response) + assert(b'ssl-specific-user@' in response) + + assert(b'SSL is used' in response) + assert(b'SSL is not being used' not in response) def test_php_ini(url, username, password, server): skip_expose_php_test = os.environ.get('SKIP_EXPOSE_PHP_TEST'); br = create_browser() response = do_login(br, url, username, password, server) + response = response.read() - assert(b'Show PHP information' in response.read()) + assert(b'Show PHP information' in response) # Open Show PHP information response = br.follow_link(text_regex=re.compile('Show PHP information')) diff --git a/testing/secure-user.sql b/testing/secure-user.sql new file mode 100644 index 0000000..e69de29 diff --git a/testing/test-docker.sh b/testing/test-docker.sh index 20a74f0..e51ff9a 100755 --- a/testing/test-docker.sh +++ b/testing/test-docker.sh @@ -1,5 +1,7 @@ #!/bin/sh +set -eu + # Set phpMyAdmin environment PHPMYADMIN_HOSTNAME=${TESTSUITE_HOSTNAME:=localhost} PHPMYADMIN_PORT=${TESTSUITE_PORT:=80} @@ -8,9 +10,24 @@ PHPMYADMIN_URL=http://${PHPMYADMIN_HOSTNAME}:${PHPMYADMIN_PORT}/ # Set database environment PHPMYADMIN_DB_HOSTNAME=${PMA_HOST:=localhost} PHPMYADMIN_DB_PORT=${PMA_PORT:=3306} +TESTSUITE_USER=${TESTSUITE_USER:=root} +TESTSUITE_ROOT_PASSWORD=${TESTSUITE_ROOT_PASSWORD:-} -if [ ! -z "${TESTSUITE_HOSTNAME_ARBITRARY}" ]; then - SERVER="--server ${PHPMYADMIN_DB_HOSTNAME}" +SUBJECT_CA="/C=US/O=phpMyAdmin testing/OU=Docker/CN=ssl-ca.phpmyadmin.local/emailAddress=ssl-ca@example.org" +SUBJECT_CLIENT="/C=US/O=phpMyAdmin testing/OU=Docker/CN=client.phpmyadmin.local/emailAddress=secure-user@example.org" + +if [ "${TESTSUITE_USER}" = "root" ] && [ -n "${TESTSUITE_ROOT_PASSWORD}" ]; then + echo "Do not use TESTSUITE_ROOT_PASSWORD with TESTSUITE_USER=root" + exit 1 +fi + +TEST_CLI_ARGS="" +if [ -n "${TESTSUITE_HOSTNAME_ARBITRARY:-}" ]; then + TEST_CLI_ARGS="$TEST_CLI_ARGS --server ${PHPMYADMIN_DB_HOSTNAME}" +fi + +if [ -n "${TESTSUITE_ROOT_PASSWORD}" ]; then + TEST_CLI_ARGS="$TEST_CLI_ARGS --root-password ${TESTSUITE_ROOT_PASSWORD}" fi # Find test script @@ -20,7 +37,45 @@ else FILENAME=./testing/phpmyadmin_test.py fi -mariadb -h "${PHPMYADMIN_DB_HOSTNAME}" -P"${PHPMYADMIN_DB_PORT}" -u"${TESTSUITE_USER:=root}" -p"${TESTSUITE_PASSWORD}" --skip-ssl -e "SELECT @@version;" > /dev/null +SSL_FLAG="--skip-ssl" + +if [ -n "${IS_USING_SSL:-}" ]; then + SSL_FLAG="--ssl --ssl-verify-server-cert --ssl-ca=/etc/phpmyadmin/ssl/ca-cert.pem" +fi + +mariadb $SSL_FLAG -h "${PHPMYADMIN_DB_HOSTNAME}" -P"${PHPMYADMIN_DB_PORT}" -u"$TESTSUITE_USER" -p"${TESTSUITE_PASSWORD}" -e "SELECT @@version;SHOW VARIABLES LIKE 'require_secure_transport';SHOW VARIABLES LIKE '%ssl%';" + +if [ -n "${IS_USING_SSL:-}" ]; then + set +e + mariadb --skip-ssl -h "${PHPMYADMIN_DB_HOSTNAME}" -P"${PHPMYADMIN_DB_PORT}" -u"$TESTSUITE_USER" -p"${TESTSUITE_PASSWORD}" -e "SELECT @@version;SHOW VARIABLES LIKE 'require_secure_transport';" 1> /dev/null 2> /dev/null + if [ $? != 1 ]; then + echo "The server does not enforce SSL connections, stopping the test." + exit 1 + fi + set -e +fi + +if [ -n "${IS_USING_SSL:-}" ] && [ -n "${TESTSUITE_ROOT_PASSWORD}" ]; then + mariadb $SSL_FLAG -h "${PHPMYADMIN_DB_HOSTNAME}" -P"${PHPMYADMIN_DB_PORT}" -u"root" -p"${TESTSUITE_ROOT_PASSWORD}" \ + -e "CREATE USER 'ssl-specific-user'@'%' REQUIRE SUBJECT '$SUBJECT_CLIENT' AND ISSUER '$SUBJECT_CA';" + + set +e + mariadb $SSL_FLAG --ssl-cert=/etc/phpmyadmin/ssl/client-cert.pem --ssl-key=/etc/phpmyadmin/ssl/client-key.pem -h "${PHPMYADMIN_DB_HOSTNAME}" -P"${PHPMYADMIN_DB_PORT}" -u"ssl-specific-user" -e "SELECT @@version;SHOW VARIABLES LIKE 'require_secure_transport';" 1> /dev/null 2> /dev/null + if [ $? != 0 ]; then + echo "The server should accept the SSL client cert login, stopping the test." + exit 1 + fi + set -e + + set +e + mariadb $SSL_FLAG -h "${PHPMYADMIN_DB_HOSTNAME}" -P"${PHPMYADMIN_DB_PORT}" -u"ssl-specific-user" -e "SELECT @@version;SHOW VARIABLES LIKE 'require_secure_transport';" 1> /dev/null 2> /dev/null + if [ $? != 1 ]; then + echo "The server should refuse the login without a client cert, stopping the test." + exit 1 + fi + set -e +fi + ret=$? if [ $ret -ne 0 ] ; then @@ -38,7 +93,7 @@ fi # Perform tests ret=0 -pytest -p no:cacheprovider -q --url "$PHPMYADMIN_URL" --username ${TESTSUITE_USER:=root} --password "$TESTSUITE_PASSWORD" $SERVER $FILENAME +pytest -p no:cacheprovider -q --url "$PHPMYADMIN_URL" --username $TESTSUITE_USER --password "$TESTSUITE_PASSWORD" $TEST_CLI_ARGS $FILENAME ret=$? # Show debug output in case of failure