diff --git a/README.md b/README.md index a7a9d741..0927eef1 100644 --- a/README.md +++ b/README.md @@ -193,8 +193,11 @@ Default configure file `overlaybd.json` is installed to `/etc/overlaybd/`. | gzipCacheConfig.cacheSizeGB | The max size of cache, in GB. | | gzipCacheConfig.refillSize | The refill size from source, in byte. `262144` is default (256 KB). | | credentialFilePath(legacy) | The credential used for fetching images on registry. `/opt/overlaybd/cred.json` is the default value. | -| credentialConfig.mode | Authentication mode for lazy-loading.
- `file` means reading credential from `credentialConfig.path`.
- `http` means sending an http request to `credentialConfig.path` | +| credentialConfig.mode | Authentication mode for lazy-loading.
- `file` means reading credential from `credentialConfig.path`.
- `http` means sending an http request to `credentialConfig.path`
- `https` means sending an https request to `credentialConfig.path`, with optional client certificate authentication and CA pinning | | credentialConfig.path | credential file path or url which is determined by `mode` | +| credentialConfig.client_cert_path | Optional. Path to the client certificate file (`https` mode). May contain the private key in the same PEM file. | +| credentialConfig.client_key_path | Optional. Path to the client private key file (`https` mode). Only needed when the key is separate from the certificate. | +| credentialConfig.server_ca_path | Optional. Path to the CA certificate used to verify the server (`https` mode). If omitted, the system CA bundle is used. When set, **only** this CA file is trusted. | | download.enable | Whether background downloading is enabled or not. | | download.delay | The seconds waiting to start downloading task after the overlaybd device launched. | | download.delayExtra | A random extra delay is attached to delay, avoiding too many tasks started at the same time. | @@ -293,6 +296,35 @@ Overlaybd supports serveral credential mode. Here are some example `credentialCo ``` we write a sample http server in `test/simple_auth_server.cpp` +- mode **https** + + the `credentialConfig.path` should be an HTTPS server listening address. Unlike `http` mode, the `https://` scheme prefix must be included in the path (e.g. `https://localhost:19876/auth`). The optional `client_cert_path`/`client_key_path` fields enable client certificate authentication, and `server_ca_path` pins trust to a specific CA. For a local auth server, providing all three fields secures communication exclusively with that server (mutual TLS). + +```json +#### /etc/overlaybd/config.json #### +{ + "logLevel": 1, + "logPath": "/var/log/overlaybd.log", + ... + "credentialConfig": { + "mode": "https", + "path": "https://localhost:19876/auth", + "client_cert_path": "/etc/overlaybd/client.crt", + "client_key_path": "/etc/overlaybd/client.key", + "server_ca_path": "/etc/overlaybd/ca.crt" + }, + ... +} +``` + overlaybd will send an https request with mTLS to the server with `remote_url` like this: +> GET "https://localhost:19876/auth?remote_url=https://hub.docker.com/v2/overlaybd/ubuntu/blobs/sha256:47e63559a8487efb55b2f1ccea9cfc04110a185c49785fdf1329d1ea462ce5f0" + the server response format is the same as the `http` mode. + + All three TLS fields are optional and independently configured: + - `client_cert_path` sets the client certificate. If the PEM file also contains the private key, `client_key_path` can be omitted. + - `client_key_path` sets the client private key. Only needed when the key is in a separate file from the certificate. + - If `server_ca_path` is omitted, the system CA bundle is used to verify the server certificate. When `server_ca_path` is set, **only** the specified CA file is used — the system CA bundle is not consulted. + ## Usage diff --git a/src/config.h b/src/config.h index d2640194..d89a210a 100644 --- a/src/config.h +++ b/src/config.h @@ -97,6 +97,9 @@ struct CredentialConfig : public ConfigUtils::Config { APPCFG_PARA(mode, std::string, ""); APPCFG_PARA(path, std::string, ""); APPCFG_PARA(timeout, int, 1); + APPCFG_PARA(client_cert_path, std::string, ""); + APPCFG_PARA(client_key_path, std::string, ""); + APPCFG_PARA(server_ca_path, std::string, ""); }; struct CacheConfig : public ConfigUtils::Config { diff --git a/src/image_service.cpp b/src/image_service.cpp index 3d39c57a..bb79f97c 100644 --- a/src/image_service.cpp +++ b/src/image_service.cpp @@ -166,6 +166,49 @@ int load_cred_from_http(const std::string addr /* http server */, const std::str return parse_auths(response.data().auths(), remote_path, username, password); } +int load_cred_from_https(const std::string addr /* https server */, const std::string &remote_path, + std::string &username, std::string &password, int timeout, + const std::string &client_cert_path, const std::string &client_key_path, + const std::string &server_ca_path) { + + auto request = new photon::net::cURL(); + DEFER({ delete request; }); + + // Configure mTLS: client certificate, client key, and server CA verification + if (!server_ca_path.empty()) { + request->set_cafile(server_ca_path.c_str()); + request->setopt(CURLOPT_SSL_VERIFYPEER, 1L).setopt(CURLOPT_SSL_VERIFYHOST, 2L); + } + if (!client_cert_path.empty()) { + request->setopt(CURLOPT_SSLCERT, client_cert_path.c_str()); + } + // When CURLOPT_SSLKEY is not set, libcurl expects the private key to be + // bundled in the same PEM file as the client certificate. + if (!client_key_path.empty()) { + request->setopt(CURLOPT_SSLKEY, client_key_path.c_str()); + } + + auto request_url = addr + "?remote_url=" + remote_path; + LOG_INFO("request url: `", request_url); + photon::net::StringWriter writer; + auto ret = request->GET(request_url.c_str(), &writer, (int64_t)timeout * 1000000); + if (ret != 200) { + LOG_ERRNO_RETURN(0, -1, "connect to auth component failed. http response code: `", ret); + } + LOG_DEBUG(writer.string); + ImageAuthResponse response; + LOG_DEBUG("response size: `", writer.string.size()); + if (response.ParseJSONStream(writer.string) == false) { + LOG_ERRNO_RETURN(0, -1, "parse http response message failed: `", writer.string); + } + LOG_INFO("traceId: `, succ: `", response.traceId(), response.success()); + if (response.success() == false) { + LOG_ERRNO_RETURN(0, -1, "http request failed."); + } + ImageConfigNS::AuthConfig cfg; + return parse_auths(response.data().auths(), remote_path, username, password); +} + int ImageService::read_global_config_and_set() { LOG_INFO("using config `", m_config_path); if (!global_conf.ParseJSON(m_config_path)) { @@ -241,6 +284,13 @@ ImageService::reload_auth(const char *remote_path) { } else if (mode == "http") { auto timeout = global_conf.credentialConfig().timeout(); res = load_cred_from_http(path, std::string(remote_path), username, password, timeout); + } else if (mode == "https") { + auto timeout = global_conf.credentialConfig().timeout(); + auto client_cert = global_conf.credentialConfig().client_cert_path(); + auto client_key = global_conf.credentialConfig().client_key_path(); + auto server_ca = global_conf.credentialConfig().server_ca_path(); + res = load_cred_from_https(path, std::string(remote_path), username, password, + timeout, client_cert, client_key, server_ca); } else { LOG_ERROR("invalid mode for authentication."); return std::make_pair("","");