diff --git a/README.md b/README.md index 34a096db..0bc65424 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ 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`
- `https` means sending an https request to `credentialConfig.path`, with optional client certificate authentication and CA pinning | +| 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
- `uds` means sending the same http request as `http` mode, but over a Unix-domain socket at `credentialConfig.path` | | 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. | @@ -325,6 +325,30 @@ we write a sample http server in `test/simple_auth_server.cpp` - `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. +- mode **uds** + + the `credentialConfig.path` should be the filesystem path of a Unix-domain socket. overlaybd dials the socket and speaks the same HTTP request/response format as the `http` mode. + +```json +#### /etc/overlaybd/config.json #### +{ + "logLevel": 1, + "logPath": "/var/log/overlaybd.log", + ... + "credentialConfig": { + "mode": "uds", + "path": "/run/overlaybd/creds.sock" + }, + ... +} +``` + overlaybd will dial the socket and send a request like: +> GET "http://localhost/auth?remote_url=https://hub.docker.com/v2/overlaybd/ubuntu/blobs/sha256:47e63559a8487efb55b2f1ccea9cfc04110a185c49785fdf1329d1ea462ce5f0" + + The HTTP host (`localhost` here) is a placeholder — the request is always dialed over the configured socket. The server response format is identical to the `http` mode. + + Security model: a UDS is not network-reachable and is gated by filesystem permissions on the socket file. Restrict access by setting the socket owner/group and a non-world-readable mode (e.g. `0600` or `0660`) on the helper side; overlaybd does not enforce this on the client. + ## Usage diff --git a/src/image_service.cpp b/src/image_service.cpp index bb79f97c..e754770a 100644 --- a/src/image_service.cpp +++ b/src/image_service.cpp @@ -209,6 +209,46 @@ int load_cred_from_https(const std::string addr /* https server */, const std::s return parse_auths(response.data().auths(), remote_path, username, password); } +int load_cred_from_uds(const std::string socket_path, const std::string &remote_path, + std::string &username, std::string &password, int timeout) { + if (socket_path.empty()) { + LOG_ERROR_RETURN(0, -1, "empty uds socket path"); + } + if (::access(socket_path.c_str(), F_OK) != 0) { + LOG_ERRNO_RETURN(0, -1, "uds socket does not exist: `", socket_path); + } + if (::access(socket_path.c_str(), W_OK) != 0) { + LOG_ERRNO_RETURN(0, -1, "uds socket not writable: `", socket_path); + } + + auto request = new photon::net::cURL(); + DEFER({ delete request; }); + + request->setopt(CURLOPT_UNIX_SOCKET_PATH, socket_path.c_str()); + + // Host is a placeholder when CURLOPT_UNIX_SOCKET_PATH is set; libcurl + // dials the socket regardless of the URL host. + auto request_url = std::string("http://localhost/auth?remote_url=") + remote_path; + LOG_INFO("request url: ` (uds: `)", request_url, socket_path); + 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)) { @@ -291,6 +331,10 @@ ImageService::reload_auth(const char *remote_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 if (mode == "uds") { + auto timeout = global_conf.credentialConfig().timeout(); + res = load_cred_from_uds(path, std::string(remote_path), + username, password, timeout); } else { LOG_ERROR("invalid mode for authentication."); return std::make_pair("",""); diff --git a/src/test/simple_credsrv_test.cpp b/src/test/simple_credsrv_test.cpp index acc33934..169c47df 100644 --- a/src/test/simple_credsrv_test.cpp +++ b/src/test/simple_credsrv_test.cpp @@ -97,6 +97,37 @@ TEST(auth, http_server) { photon::thread_sleep(60); } +TEST(auth, uds_server) { + const char *sock_path = "/tmp/obd-credsrv-uds-test.sock"; + ::unlink(sock_path); + + auto udsserver = photon::net::new_uds_server(true /* autoremove */); + udsserver->timeout(1000UL * 1000); + ASSERT_EQ(0, udsserver->bind(sock_path)); + ASSERT_EQ(0, udsserver->listen()); + DEFER(delete udsserver); + + auto server = new_http_server(); + DEFER(delete server); + SimpleAuthHandler h; + server->add_handler(&h, false, "/auth"); + udsserver->set_handler(server->get_connection_handler()); + udsserver->start_loop(); + photon::thread_sleep(1); + + std::string remote_path = "", user = "", passwd = ""; + // The handler sleeps 1s before responding; a 1s timeout must trip. + auto ret = load_cred_from_uds(sock_path, remote_path, user, passwd, 1); + EXPECT_EQ(ret, -1); + ret = load_cred_from_uds(sock_path, remote_path, user, passwd, 3); + EXPECT_EQ(ret, 0); + + // Missing socket file must surface as a clear error. + ret = load_cred_from_uds("/tmp/obd-credsrv-uds-test.does-not-exist.sock", + remote_path, user, passwd, 1); + EXPECT_EQ(ret, -1); +} + int main(int argc, char** arg) { photon::init(); DEFER(photon::fini());