Skip to content
Open
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
12 changes: 11 additions & 1 deletion src/libstore/filetransfer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,9 @@ struct curlFileTransfer : public FileTransfer
curl_easy_setopt(req, CURLOPT_PIPEWAIT, 1);
#endif
#if LIBCURL_VERSION_NUM >= 0x072f00
if (fileTransferSettings.enableHttp2)
// Our writeCallbackWrapper does not support rewinding which breaks
// negotiate/kerberos auth over http/2.
Comment on lines +354 to +355
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Our writeCallbackWrapper does not support rewinding which breaks
// negotiate/kerberos auth over http/2.
// Our writeCallbackWrapper does not support rewinding which breaks
// negotiate/kerberos auth over http/2.
// Curl would need to retry a request, after consuming part of our stream, and we currently don't have
// a way to recover that initial already consumed part.

if (fileTransferSettings.enableHttp2 && request.authmethod != HttpAuthMethod::NEGOTIATE)
curl_easy_setopt(req, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
else
curl_easy_setopt(req, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
Expand Down Expand Up @@ -397,6 +399,14 @@ struct curlFileTransfer : public FileTransfer
curl_easy_setopt(req, CURLOPT_SOCKOPTFUNCTION, cloexec_callback);
#endif

curl_easy_setopt(req, CURLOPT_HTTPAUTH, request.authmethod);
if (request.authmethod == HttpAuthMethod::NEGOTIATE) {
curl_easy_setopt(req, CURLOPT_USERNAME, "");
curl_easy_setopt(req, CURLOPT_PASSWORD, "");
Comment on lines +404 to +405
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
curl_easy_setopt(req, CURLOPT_USERNAME, "");
curl_easy_setopt(req, CURLOPT_PASSWORD, "");
// Initialize the auth stack. It needs to have a username, password to trigger that. Otherwise kerberos isn't going to work.
curl_easy_setopt(req, CURLOPT_USERNAME, "");
curl_easy_setopt(req, CURLOPT_PASSWORD, "");

} else if (request.authmethod == HttpAuthMethod::BEARER) {
curl_easy_setopt(req, CURLOPT_XOAUTH2_BEARER, request.bearer_token.c_str());
}

curl_easy_setopt(req, CURLOPT_CONNECTTIMEOUT, fileTransferSettings.connectTimeout.get());

curl_easy_setopt(req, CURLOPT_LOW_SPEED_LIMIT, 1L);
Expand Down
23 changes: 21 additions & 2 deletions src/libstore/http-binary-cache-store.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@ namespace nix {

MakeError(UploadToHTTP, Error);

HttpAuthMethod parseHttpAuthMethod(const std::string &str) {
static const std::map<std::string, HttpAuthMethod> map = {
{"none", HttpAuthMethod::NONE},
{"basic", HttpAuthMethod::BASIC},
{"digest", HttpAuthMethod::DIGEST},
{"negotiate", HttpAuthMethod::NEGOTIATE},
{"ntlm", HttpAuthMethod::NTLM},
{"bearer", HttpAuthMethod::BEARER},
{"any", HttpAuthMethod::ANY},
{"anysafe", HttpAuthMethod::ANYSAFE}};
auto it = map.find(str);
if (it == map.end()) {
throw UsageError("option authmethod has invalid value '%s'", str);
}
return it->second;
}


StringSet HttpBinaryCacheStoreConfig::uriSchemes()
{
Expand Down Expand Up @@ -152,11 +169,13 @@ class HttpBinaryCacheStore :

FileTransferRequest makeRequest(const std::string & path)
{
return FileTransferRequest(
auto request = FileTransferRequest(
hasPrefix(path, "https://") || hasPrefix(path, "http://") || hasPrefix(path, "file://")
? path
: config->cacheUri + "/" + path);

request.authmethod = parseHttpAuthMethod(config->authmethod);
request.bearer_token = config->bearer_token;
return request;
}

void getFile(const std::string & path, Sink & sink) override
Expand Down
4 changes: 4 additions & 0 deletions src/libstore/include/nix/store/filetransfer.hh
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once
///@file

#include "http-binary-cache-store.hh"

#include <string>
#include <future>

Expand Down Expand Up @@ -68,6 +70,8 @@ struct FileTransferRequest
bool verifyTLS = true;
bool head = false;
bool post = false;
HttpAuthMethod authmethod = HttpAuthMethod::BASIC;
std::string bearer_token;
size_t tries = fileTransferSettings.tries;
unsigned int baseRetryTimeMs = RETRY_TIME_MS_DEFAULT;
ActivityId parentAct;
Expand Down
28 changes: 27 additions & 1 deletion src/libstore/include/nix/store/http-binary-cache-store.hh
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
#include "nix/store/binary-cache-store.hh"
#pragma once
///@file

#include "nix/util/types.hh"
#include <curl/curl.h>

#include "binary-cache-store.hh"

namespace nix {

enum struct HttpAuthMethod : unsigned long {
NONE = CURLAUTH_NONE,
BASIC = CURLAUTH_BASIC,
DIGEST = CURLAUTH_DIGEST,
NEGOTIATE = CURLAUTH_NEGOTIATE,
NTLM = CURLAUTH_NTLM,
BEARER = CURLAUTH_BEARER,
ANY = CURLAUTH_ANY,
ANYSAFE = CURLAUTH_ANYSAFE
};

struct HttpBinaryCacheStoreConfig : std::enable_shared_from_this<HttpBinaryCacheStoreConfig>,
virtual Store::Config,
BinaryCacheStoreConfig
Expand All @@ -20,6 +37,15 @@ struct HttpBinaryCacheStoreConfig : std::enable_shared_from_this<HttpBinaryCache

static StringSet uriSchemes();

const Setting<std::string> authmethod{this, "basic", "authmethod",
R"(
libcurl auth method to use (`none`, `basic`, `digest`, `bearer`, `negotiate`, `ntlm`, `any`, or `anysafe`).
See https://curl.se/libcurl/c/CURLOPT_HTTPAUTH.html for more info.
)"};

const Setting<std::string> bearer_token{this, "", "bearer-token",
"Bearer token to use for authentication. Requires `authmethod` to be set to `bearer`."};

static std::string doc();

ref<Store> openStore() const override;
Expand Down
1 change: 1 addition & 0 deletions src/libstore/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ mkMesonLibrary (finalAttrs: {
propagatedBuildInputs = [
nix-util
nlohmann_json
curl
];

mesonFlags =
Expand Down
2 changes: 2 additions & 0 deletions tests/nixos/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ in

s3-binary-cache-store = runNixOSTestFor "x86_64-linux" ./s3-binary-cache-store.nix;

http-binary-cache-auth = runNixOSTestFor "x86_64-linux" ./http-binary-cache-auth.nix;

fsync = runNixOSTestFor "x86_64-linux" ./fsync.nix;

cgroups = runNixOSTestFor "x86_64-linux" ./cgroups;
Expand Down
245 changes: 245 additions & 0 deletions tests/nixos/http-binary-cache-auth.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
# Run with:
# cd nixpkgs
# nix-build -A nixosTests.http-binary-cache-auth

# Test HTTP binary cache authentication methods (Basic, Bearer)
{
lib,
config,
nixpkgs,
...
}:

let
pkgs = config.nodes.client.nixpkgs.pkgs;

# Test package to copy
testPkg = pkgs.hello;

# Basic auth credentials
basicUser = "testuser";
basicPassword = "testpassword";

# Bearer token for bearer auth test
bearerToken = "test-bearer-token-12345";

in
{
name = "http-binary-cache-auth";

nodes = {
# HTTP Binary Cache Server with authentication
server =
{ config, pkgs, ... }:
{
virtualisation.writableStore = true;
virtualisation.additionalPaths = [ testPkg ];

networking.firewall.allowedTCPPorts = [
80
8080
8081
];

# Nginx with different auth methods
services.nginx = {
enable = true;

# For basic auth testing
virtualHosts."basic-auth" = {
listen = [
{
addr = "0.0.0.0";
port = 8080;
}
];

locations."/" = {
root = "/var/cache/nix-cache";
extraConfig = ''
autoindex on;
auth_basic "Nix Binary Cache";
auth_basic_user_file /etc/nginx/htpasswd;
'';
};
};

# For bearer token auth testing
virtualHosts."bearer-auth" = {
listen = [
{
addr = "0.0.0.0";
port = 8081;
}
];

locations."/" = {
root = "/var/cache/nix-cache";
extraConfig = ''
autoindex on;
set $auth_header $http_authorization;
if ($auth_header != "Bearer ${bearerToken}") {
return 401;
}
'';
};
};

# No auth (for control test)
virtualHosts."no-auth" = {
listen = [
{
addr = "0.0.0.0";
port = 80;
}
];

locations."/" = {
root = "/var/cache/nix-cache";
extraConfig = ''
autoindex on;
'';
};
};
};

environment.systemPackages = [ pkgs.curl ];

nix.settings.substituters = lib.mkForce [ ];
};

# Client machine
client =
{ config, pkgs, ... }:
{
virtualisation.writableStore = true;

environment.systemPackages = [ pkgs.curl ];

nix.settings.substituters = lib.mkForce [ ];
};
};

testScript =
{ nodes }:
''
import time

# fmt: off
start_all()

# Wait for services to be ready
server.wait_for_unit("nginx.service")
server.wait_for_open_port(80)
server.wait_for_open_port(8080)
server.wait_for_open_port(8081)

# Set up basic auth htpasswd file
server.succeed("""
echo '${basicPassword}' | htpasswd -i -c /etc/nginx/htpasswd ${basicUser}
systemctl reload nginx
""")

# Create binary cache on server
server.succeed("""
mkdir -p /var/cache/nix-cache
nix copy --to file:///var/cache/nix-cache ${testPkg}
""")

# Test that the binary cache was created properly
server.succeed("ls -la /var/cache/nix-cache/")
server.succeed("ls -la /var/cache/nix-cache/nar/")

# Test 1: No authentication (control)
print("Testing no authentication...")
client.succeed("curl -f http://server/")
client.succeed("""
nix copy --from 'http://server' ${testPkg} --no-check-sigs
""")
client.succeed("nix path-info ${testPkg}")
client.succeed("nix-store --delete ${testPkg}")

# Test 2: Basic authentication
print("Testing Basic authentication...")

# Verify we can't access without auth
client.fail("curl -f http://server:8080/")

# Test curl with basic auth works
client.succeed("curl -f -u ${basicUser}:${basicPassword} http://server:8080/")

# Test nix with basic auth (default authmethod)
# Note: Nix uses libcurl which reads credentials from .netrc
client.succeed("""
cat > ~/.netrc << EOF
machine server
login ${basicUser}
password ${basicPassword}
EOF
chmod 600 ~/.netrc
""")

client.succeed("""
nix copy --from 'http://server:8080' ${testPkg} --no-check-sigs
""")

client.succeed("nix path-info ${testPkg}")
client.succeed("nix-store --delete ${testPkg}")
client.succeed("rm ~/.netrc")

# Test 3: Bearer token authentication
print("Testing Bearer token authentication...")

# Verify we can't access without auth
client.fail("curl -f http://server:8081/")

# Test curl with bearer token
client.succeed('curl -f -H "Authorization: Bearer ${bearerToken}" http://server:8081/')

# Test nix with bearer auth
client.succeed("""
nix copy --from 'http://server:8081?authmethod=bearer&bearer-token=${bearerToken}' ${testPkg} --no-check-sigs
""")

client.succeed("nix path-info ${testPkg}")
client.succeed("nix-store --delete ${testPkg}")

# Test 4: Wrong bearer token should fail
print("Testing bearer token failure...")
client.fail("""
nix copy --from 'http://server:8081?authmethod=bearer&bearer-token=wrong-token' ${testPkg} --no-check-sigs 2>&1
""")

# Test 5: Using wrong auth method should fail
print("Testing auth method mismatch...")
# Try basic auth on bearer endpoint
client.fail("""
cat > ~/.netrc << EOF
machine server
login ${basicUser}
password ${basicPassword}
EOF
chmod 600 ~/.netrc
nix copy --from 'http://server:8081?authmethod=basic' ${testPkg} --no-check-sigs 2>&1
""")
client.succeed("rm ~/.netrc")

# Test 6: Test that authmethod parameter is parsed correctly
print("Testing authmethod parameter parsing...")

# Valid auth methods should not error on parsing
for method in ["basic", "digest", "negotiate", "ntlm", "bearer", "any", "anysafe"]:
# We expect these to fail due to auth, but not due to parsing
client.fail(f"""
timeout 5 nix copy --from 'http://server:8080?authmethod={method}' ${testPkg} --no-check-sigs 2>&1 | grep -v "unknown auth method"
""")

# Invalid auth method should error
result = client.fail("""
nix copy --from 'http://server:8080?authmethod=invalid' ${testPkg} --no-check-sigs 2>&1
""")
assert "unknown auth method 'invalid'" in result

print("All authentication tests passed!")
'';
}