diff --git a/contrib/plugins/meson.build b/contrib/plugins/meson.build deleted file mode 100644 index c40e6175a..000000000 --- a/contrib/plugins/meson.build +++ /dev/null @@ -1,14 +0,0 @@ -plugin_mtls_store = shared_module( - 'plugin_mtls_store', - 'plugin_mtls_store.cc', - dependencies : [ - liblixutil, - liblixstore, - liblixexpr, - liblixfetchers, - curl, - ], - install : false, - build_by_default : true, - link_args : strict_shared_module_link_args, -) diff --git a/contrib/plugins/mtls-http-binary-cache-store.md b/contrib/plugins/mtls-http-binary-cache-store.md deleted file mode 100644 index 1274f7b61..000000000 --- a/contrib/plugins/mtls-http-binary-cache-store.md +++ /dev/null @@ -1,13 +0,0 @@ -R"( - -**Store URL format**: `https+mtls://...` - -This store allows a binary cache to be accessed via the HTTPS -protocol with mutual TLS mandated. - -Two parameters can be passed to the query string: - -- `tls-certificate`, a path to the TLS client certificate (optional) -- `tls-private-key`, a path to the TLS private key backing the client certificate (required) - -)" diff --git a/contrib/plugins/plugin_mtls_store.cc b/contrib/plugins/plugin_mtls_store.cc deleted file mode 100644 index 759301323..000000000 --- a/contrib/plugins/plugin_mtls_store.cc +++ /dev/null @@ -1,102 +0,0 @@ -#include "lix/libstore/store-api.hh" -#include "lix/libutil/config.hh" -#include "lix/libstore/http-binary-cache-store.hh" -#include -#include - -namespace nix { -struct mTLSBinaryCacheStoreConfig : HttpBinaryCacheStoreConfig -{ - using HttpBinaryCacheStoreConfig::HttpBinaryCacheStoreConfig; - - const std::string name() override - { - return "mTLS HTTP Binary Cache Store"; - } - - std::string doc() override - { - return -#include "mtls-http-binary-cache-store.md" - ; - } - - PathsSetting tlsCertificate{ - this, - "", - "tls-certificate", - "Path of an optional TLS client certificate in PEM format as expected by CURLOPT_SSLCERT" - }; - - PathsSetting tlsKey{ - this, - "", - "tls-private-key", - "Path of an TLS client certificate private key in PEM format as expected by CURLOPT_SSLKEY" - }; -}; - -struct mTLSBinaryCacheStoreImpl : public HttpBinaryCacheStore -{ - struct Keyring - { - nix::Path tlsCertificate; - nix::Path tlsKey; - }; - - mTLSBinaryCacheStoreConfig config_; - std::shared_ptr keyring; - - mTLSBinaryCacheStoreConfig & config() override - { - return config_; - } - const mTLSBinaryCacheStoreConfig & config() const override - { - return config_; - } - - mTLSBinaryCacheStoreImpl( - const std::string & uriScheme, const Path & _cacheUri, mTLSBinaryCacheStoreConfig config - ) - : Store(config) - , HttpBinaryCacheStore("https", _cacheUri, config) - , config_(std::move(config)) - , keyring(std::make_shared(config_.tlsCertificate.get(), config_.tlsKey.get())) - { - } - - FileTransferOptions makeOptions(Headers && headers = {}) override - { - auto options = HttpBinaryCacheStore::makeOptions(std::move(headers)); - auto baseExtraSetup = std::move(options.extraSetup); - auto keyring = this->keyring; - - options.extraSetup = - [keyring, baseExtraSetup{std::move(baseExtraSetup)}](CURL * req) { - if (baseExtraSetup) { - baseExtraSetup(req); - } - - if (!keyring->tlsCertificate.empty()) { - curl_easy_setopt(req, CURLOPT_SSLCERT, keyring->tlsCertificate.c_str()); - } - - curl_easy_setopt(req, CURLOPT_SSLKEY, keyring->tlsKey.c_str()); - }; - - return options; - } - - static std::set uriSchemes() - { - return {"https+mtls"}; - } -}; -} - - -extern "C" void nix_plugin_entry() -{ - nix::StoreImplementations::add(); -} diff --git a/doc/manual/change-authors.yml b/doc/manual/change-authors.yml index 151c69b70..c4d212fd0 100644 --- a/doc/manual/change-authors.yml +++ b/doc/manual/change-authors.yml @@ -156,9 +156,6 @@ ma27: matthewbauer: github: matthewbauer -mic92: - github: Mic92 - midnightveil: display_name: julia forgejo: midnightveil @@ -246,9 +243,6 @@ vigress8: forgejo: vigress8 github: vigress8 -vlaci: - github: vlaci - vlinkz: display_name: Victor Fuentes forgejo: vlinkz diff --git a/doc/manual/rl-next/mtls-plugin.md b/doc/manual/rl-next/mtls-plugin.md deleted file mode 100644 index 1dc284535..000000000 --- a/doc/manual/rl-next/mtls-plugin.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -synopsis: "mTLS store connections via a plugin" -issues: [] -cls: [3754, 3696, 3697, 3698] -category: Improvements -credits: [raito, horrors, mic92, vlaci] ---- - -To support use cases requiring mutual TLS (mTLS) authentication when connecting -to remote Nix stores, e.g. private stores, we have introduced a **contributed** -mTLS plugin extending the Lix store interface. - -This design follows an extensibility model which was brought up [by a proposal -of making Kerberos authentication possible in Lix -directly](https://gerrit.lix.systems/c/lix/+/3637). - -This mTLS plugin serves as a concrete example of how store connection -mechanisms can be modularized through external plugins, without extending Lix -core. This idea can be generalized to integrate automatic certificate renewal -or advanced integrations with secrets engine or posture checks. - -It enables custom TLS client certificates to be used for authenticating against -a remote store that enforces mTLS. - -To use the plugin, configure Lix manually by setting in your `nix.conf`: - -``` -plugin-files = /a/path/to/libplugin_mtls_store.so -``` - -Currently, this must be done explicitly. In the future, Nixpkgs will provide a -mechanism to reference an up-to-date and curated set of plugins automatically. - -Making plugins easily consumable outside of Nixpkgs (e.g., from external plugin -registries or binary distributions) remains an open question and will require -further design. - -Contributed plugins come with significantly reduced **stability** and -**maintenance** guarantees compared to the Lix core. We encourage users who -depend on a given plugin to take on maintenance responsibilities and apply for -ownership within the Lix mono-repository. These plugins are subject to removal -at any time. diff --git a/meson.build b/meson.build index 874ba43e6..222c4db79 100644 --- a/meson.build +++ b/meson.build @@ -125,7 +125,6 @@ endif enable_nix_eval_jobs = get_option('nix-eval-jobs') enable_tests = get_option('enable-tests') -enable_contrib_plugins = get_option('enable-contrib-plugins') tests_args = [] @@ -225,13 +224,12 @@ is_x64 = host_machine.cpu_family() == 'x86_64' # This corresponds to the $(1)_ALLOW_UNDEFINED option from the Make buildsystem. # Mostly this is load-bearing on the plugin tests defined in tests/functional/plugins/meson.build. shared_module_link_args = [] -# This is a stricter additional set of link flags. -strict_shared_module_link_args = [] if is_darwin shared_module_link_args += ['-undefined', 'suppress', '-flat_namespace'] - strict_shared_module_link_args += ['-flat_namespace'] elif is_linux - strict_shared_module_link_args += ['-Wl,-z,defs'] + # -Wl,-z,defs is the equivalent, but a comment in the Make buildsystem says that breaks + # Clang sanitizers on Linux. + # FIXME(Qyriad): is that true? endif configdata = { } @@ -665,10 +663,6 @@ if enable_tests subdir('tests/functional2') endif -if enable_contrib_plugins - subdir('contrib/plugins') -endif - subdir('meson/clang-tidy') subproject('nix-eval-jobs', required : enable_nix_eval_jobs) diff --git a/meson.options b/meson.options index 0d837179e..50caa32c4 100644 --- a/meson.options +++ b/meson.options @@ -32,10 +32,6 @@ option('enable-tests', type : 'boolean', value : true, description : 'whether to enable tests or not (requires rapidcheck and gtest)', ) -option('enable-contrib-plugins', type : 'boolean', value : true, - description : 'whether to build contributed plugins' -) - option('tests-color', type : 'boolean', value : true, description : 'set to false to disable color output in gtest', ) diff --git a/misc/pre-commit.nix b/misc/pre-commit.nix index 44c35067c..51be9cabf 100644 --- a/misc/pre-commit.nix +++ b/misc/pre-commit.nix @@ -36,7 +36,7 @@ pre-commit-run { enable = true; package = pkgs.llvmPackages.libclang.python; entry = "${pkgs.llvmPackages.libclang.python}/bin/git-clang-format --binary ${pkgs.llvmPackages.clang-tools}/bin/clang-format"; - files = "^(lix/|tests/|contrib/plugins/)"; + files = "^(lix/|tests/)"; types = [ "c++" "file" diff --git a/package.nix b/package.nix index 83e109dd6..4e367fa96 100644 --- a/package.nix +++ b/package.nix @@ -232,7 +232,6 @@ stdenv.mkDerivation (finalAttrs: { ./doc ./lix ./misc - ./contrib/plugins ./COPYING ] ++ lib.optionals lintInsteadOfBuild [ ./.clang-tidy ] @@ -310,10 +309,6 @@ stdenv.mkDerivation (finalAttrs: { jq yq lsof - # For mTLS tests. - curl - openssl - python3 ] ++ lib.optional hostPlatform.isLinux util-linuxMinimal ++ lib.optional (!officialRelease && buildUnreleasedNotes) build-release-notes diff --git a/tests/functional/init.sh b/tests/functional/init.sh index aa0bac187..33ae8b2bd 100755 --- a/tests/functional/init.sh +++ b/tests/functional/init.sh @@ -10,7 +10,7 @@ if test -d "$TEST_ROOT"; then killDaemon rm -rf "$TEST_ROOT" fi -mkdir -p "$TEST_ROOT" +mkdir "$TEST_ROOT" mkdir "$NIX_STORE_DIR" mkdir "$NIX_LOCALSTATE_DIR" diff --git a/tests/functional/meson.build b/tests/functional/meson.build index 93b1f1bc1..c37a32e62 100644 --- a/tests/functional/meson.build +++ b/tests/functional/meson.build @@ -180,12 +180,6 @@ if get_option('default_library') != 'static' functional_tests_scripts += ['plugins.sh'] endif -if get_option('default_library') != 'static' and get_option('enable-contrib-plugins') - functional_tests_scripts += [ - 'plugins/mtls/substituter-ssl-client-cert.sh' - ] -endif - # TODO(Qyriad): this will hopefully be able to be removed when we remove the autoconf+Make # buildsystem. See the comments at the top of setup-functional-tests.py for why this is here. meson.add_install_script( diff --git a/tests/functional/plugins/mtls/nix-binary-cache-ssl-server.py b/tests/functional/plugins/mtls/nix-binary-cache-ssl-server.py deleted file mode 100755 index 5bf4cbbba..000000000 --- a/tests/functional/plugins/mtls/nix-binary-cache-ssl-server.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -import http.server -import ssl -import socketserver -import sys -import os -import argparse -import textwrap -from typing import Any - -class NixCacheHandler(http.server.BaseHTTPRequestHandler): - protocol_version: str = 'HTTP/1.1' - - def do_GET(self) -> None: - # Get client certificate information - try: - client_cert: dict[str, Any] | None = self.request.getpeercert() - except Exception as e: - print(f"Error getting client certificate: {e}", file=sys.stderr) - self.send_error(403, "Invalid client certificate") - return - - if not client_cert: - self.send_error(403, "No client certificate provided") - return - - # Additional validation - check if certificate chain is valid - subject: tuple[tuple[tuple[str, str], ...], ...] | None = client_cert.get('subject') - if not subject: - self.send_error(403, "Invalid client certificate: No subject") - return - - # Log client info - print(f"Client connected: {subject}", file=sys.stderr) - print(f"Path requested: {self.path}", file=sys.stderr) - - # Handle nix-cache-info endpoint - if self.path == '/nix-cache-info': - self.send_response(200) - self.send_header('Content-Type', 'text/plain') - self.send_header('Connection', 'close') # Explicitly close after response - test_root: str | None = os.environ.get('TEST_ROOT') - if not test_root: - store_root: str = '/nix/store' - else: - store_root = os.path.join(test_root, 'store') - - # Nix cache info format - cache_info: str = textwrap.dedent(f"""\ - StoreDir: {store_root} - WantMassQuery: 1 - Priority: 30 - """) - self.send_header('Content-Length', str(len(cache_info))) - self.end_headers() - self.wfile.write(cache_info.encode()) - self.wfile.flush() # Ensure data is sent - - # Handle .narinfo requests - elif self.path.endswith('.narinfo'): - # Return 404 for all narinfo requests (empty cache) - self.send_response(404) - self.send_header('Content-Length', '0') - self.send_header('Connection', 'close') - self.end_headers() - - else: - self.send_response(404) - self.send_header('Content-Length', '0') - self.send_header('Connection', 'close') - self.end_headers() - - def log_message(self, format: str, *args: Any) -> None: - # Suppress standard logging - pass - -def run_server(port_fifo_path: str, certfile: str, keyfile: str, ca_certfile: str) -> None: - # Create SSL context - context: ssl.SSLContext = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - context.load_cert_chain(certfile=certfile, keyfile=keyfile) - context.verify_mode = ssl.VerifyMode.CERT_REQUIRED - context.check_hostname = False # We're not checking hostnames for client certs - context.load_verify_locations(cafile=ca_certfile) - - # Bind to a free port - with socketserver.TCPServer(("localhost", 0), NixCacheHandler) as httpd: - port = httpd.server_address[1] # Extract chosen port - - # Wrap with TLS - httpd.socket = context.wrap_socket(httpd.socket, server_side=True) - - # Write the port to the FIFO - with open(port_fifo_path, 'w') as fifo: - fifo.write(f"{port}\n") - fifo.flush() - - print(f"Server running on port {port}", file=sys.stderr) - - try: - httpd.serve_forever() - except KeyboardInterrupt: - httpd.shutdown() - -if __name__ == "__main__": - parser: argparse.ArgumentParser = argparse.ArgumentParser(description='Nix binary cache server with SSL client verification') - parser.add_argument('--port-fifo', type=str, required=True, help="FIFO where to inform about the port taken") - parser.add_argument('--cert', required=True, help='Server certificate file') - parser.add_argument('--key', required=True, help='Server private key file') - parser.add_argument('--ca-cert', required=True, help='CA certificate for client verification') - - args: argparse.Namespace = parser.parse_args() - - run_server(args.port_fifo, args.cert, args.key, args.ca_cert) diff --git a/tests/functional/plugins/mtls/substituter-ssl-client-cert.sh b/tests/functional/plugins/mtls/substituter-ssl-client-cert.sh deleted file mode 100755 index 17ab873ee..000000000 --- a/tests/functional/plugins/mtls/substituter-ssl-client-cert.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env bash - -# shellcheck source=common.sh -source ../../common.sh - -# Load the mTLS plugin for these tests. -loadContribPlugin "mtls_store" - -# Generate test certificates using EC keys for faster generation - -# Generate CA with EC key -openssl ecparam -genkey -name prime256v1 -out "$TEST_ROOT/ca.key" 2>/dev/null -openssl req -new -x509 -days 1 -key "$TEST_ROOT/ca.key" -out "$TEST_ROOT/ca.crt" \ - -subj "/C=US/ST=Test/L=Test/O=TestCA/CN=Test CA" 2>/dev/null - -# Generate server certificate with EC key -openssl ecparam -genkey -name prime256v1 -out "$TEST_ROOT/server.key" 2>/dev/null -openssl req -new -key "$TEST_ROOT/server.key" -out "$TEST_ROOT/server.csr" \ - -subj "/C=US/ST=Test/L=Test/O=TestServer/CN=localhost" 2>/dev/null -openssl x509 -req -days 1 -in "$TEST_ROOT/server.csr" -CA "$TEST_ROOT/ca.crt" -CAkey "$TEST_ROOT/ca.key" \ - -set_serial 01 -out "$TEST_ROOT/server.crt" 2>/dev/null - -# Generate client certificate with EC key -openssl ecparam -genkey -name prime256v1 -out "$TEST_ROOT/client.key" 2>/dev/null -openssl req -new -key "$TEST_ROOT/client.key" -out "$TEST_ROOT/client.csr" \ - -subj "/C=US/ST=Test/L=Test/O=TestClient/CN=Nix Test Client" 2>/dev/null -openssl x509 -req -days 1 -in "$TEST_ROOT/client.csr" -CA "$TEST_ROOT/ca.crt" -CAkey "$TEST_ROOT/ca.key" \ - -set_serial 02 -out "$TEST_ROOT/client.crt" 2>/dev/null - -# Start the server and have it write its chosen port to the FIFO -FIFO_PATH="$TEST_ROOT/server-port.fifo" -mkfifo "$FIFO_PATH" -python3 "$PWD/nix-binary-cache-ssl-server.py" \ - --port-fifo "$FIFO_PATH" \ - --cert "$TEST_ROOT/server.crt" \ - --key "$TEST_ROOT/server.key" \ - --ca-cert "$TEST_ROOT/ca.crt" & -SERVER_PID=$! - -# Function to stop server on exit -stopServer() { - kill "$SERVER_PID" 2>/dev/null || true - wait "$SERVER_PID" 2>/dev/null || true - rm -f "$FIFO_PATH" -} -trap stopServer EXIT - -# Read port from the FIFO (waits until server writes to it) but timeouts after 5s. -if ! PORT=$(timeout 5s bash -c "read -r line < '$FIFO_PATH'; echo \"\$line\""); then - echo "Timed out waiting for server to write port to FIFO" >&2 - exit 1 -fi - -if ! curl -sSf -k --cert "$TEST_ROOT/client.crt" --key "$TEST_ROOT/client.key" \ - "https://localhost:$PORT/nix-cache-info" > /dev/null; then - if kill -0 "$SERVER_PID" 2>/dev/null; then - echo "Server started but did not respond to curl" >&2 - else - echo "Server failed to start" >&2 - fi - exit 1 -fi - -# Test 1: Verify server rejects connections without client certificate -echo "Testing connection without client certificate (should fail)..." >&2 -if curl -s -k "https://localhost:$PORT/nix-cache-info" 2>&1 | grep -q "certificate required"; then - echo "FAIL: Server should have rejected connection" >&2 - exit 1 -fi - -# Test 2: Verify server accepts connections with client certificate -echo "Testing connection with client certificate..." >&2 -RESPONSE=$(curl -v -s -k --cert "$TEST_ROOT/client.crt" --key "$TEST_ROOT/client.key" \ - "https://localhost:$PORT/nix-cache-info") - -if ! echo "$RESPONSE" | grepQuiet "StoreDir: "; then - echo "FAIL: Server should have accepted client certificate: $RESPONSE" >&2 - exit 1 -fi - -# Test 3: Test Nix with SSL client certificate parameters -# Set up substituter URL with SSL parameters -sslCache="https+mtls://localhost:$PORT?tls-certificate=$TEST_ROOT/client.crt&tls-private-key=$TEST_ROOT/client.key" - -# Configure Nix to trust our CA -export NIX_SSL_CERT_FILE="$TEST_ROOT/ca.crt" - -# Test nix store info -nix store ping --store "$sslCache" --json # | jq -e '.url' | grepQuiet "https://localhost:$PORT" - -# Test 4: Verify incorrect client certificate is rejected -# Generate a different client cert not signed by our CA (also using EC) -openssl ecparam -genkey -name prime256v1 -out "$TEST_ROOT/wrong.key" 2>/dev/null -openssl req -new -x509 -days 1 -key "$TEST_ROOT/wrong.key" -out "$TEST_ROOT/wrong.crt" \ - -subj "/C=US/ST=Test/L=Test/O=Wrong/CN=Wrong Client" 2>/dev/null - -wrongCache="https+mtls://localhost:$PORT?tls-certificate=$TEST_ROOT/wrong.crt&tls-private-key=$TEST_ROOT/wrong.key" - -rm -rf "$TEST_HOME" - -# This should fail -if nix store ping --download-attempts 0 --store "$wrongCache"; then - echo "FAIL: Should have rejected wrong certificate" >&2 - exit 1 -fi