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
4 changes: 4 additions & 0 deletions docs/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ Configuration options for the server are defined only via command-line options a
| `rest_port` | `integer` | Number of the port used by HTTP server (if not provided or set to 0, HTTP server will not be launched). |
| `grpc_bind_address` | `string` | Comma separated list of ipv4/ipv6 network interface addresses or hostnames, to which gRPC server will bind to. Default: all interfaces: 0.0.0.0 |
| `rest_bind_address` | `string` | Comma separated list of ipv4/ipv6 network interface addresses or hostnames, to which REST server will bind to. Default: all interfaces: 0.0.0.0 |
| `grpc_certificate_path` | `string` | Path to a PEM server certificate to enable TLS on the gRPC endpoint. Must be set together with `grpc_key_path`. When unset, gRPC is served in plaintext. |
| `grpc_key_path` | `string` | Path to the PEM private key matching `grpc_certificate_path`. Required to enable gRPC TLS. |
| `grpc_ca_path` | `string` | Optional path to a PEM CA certificate. When set, enables mutual TLS (mTLS) on the gRPC endpoint — clients must present a certificate signed by this CA, which is required and verified. Requires `grpc_certificate_path` and `grpc_key_path`. |
| `rest_certificate_path` / `rest_key_path` / `rest_ca_path` | `string` | TLS for the REST endpoint. **Note:** native REST HTTPS is not enabled in the current build (the bundled Drogon is built without OpenSSL); setting these parameters is rejected at startup to avoid silently serving plaintext. Use gRPC TLS, or terminate REST TLS with a reverse proxy. See [issue #2144](https://github.com/openvinotoolkit/model_server/issues/2144). |
| `grpc_workers` | `integer` | Number of the gRPC server instances (must be from 1 to CPU core count). Default value is 1 and it's optimal for most use cases. Consider setting higher value while expecting heavy load. |
| `rest_workers` | `integer` | Number of HTTP server threads. Effective when `rest_port` > 0. Default value is set based on the number of CPUs. |
| `file_system_poll_wait_seconds` | `integer` | Time interval between config and model versions changes detection in seconds. Default value is 1. Zero value disables changes monitoring. |
Expand Down
4 changes: 3 additions & 1 deletion docs/security_considerations.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ docker run --rm -d --user $(id -u):$(id -g) --read-only --tmpfs /tmp -p 9000:900

```
---
OpenVINO Model Server currently does not provide access restrictions and traffic encryption on gRPC and REST API endpoints. The endpoints can be secured using network settings like docker network settings or network firewall on the host. The recommended configuration is to place OpenVINO Model Server behind any reverse proxy component or load balancer, which provides traffic encryption and user authorization.
OpenVINO Model Server does not provide user authorization on the gRPC and REST API endpoints. The endpoints can be secured using network settings like docker network settings or network firewall on the host. The recommended configuration is to place OpenVINO Model Server behind any reverse proxy component or load balancer, which provides traffic encryption and user authorization.

The gRPC endpoint additionally supports native TLS traffic encryption. Provide a PEM server certificate and key via `--grpc_certificate_path` and `--grpc_key_path` to serve gRPC over TLS, and optionally `--grpc_ca_path` to require and verify client certificates (mutual TLS). See [parameters](parameters.md). Native REST HTTPS is not enabled in the current build (setting `--rest_certificate_path`/`--rest_key_path` is rejected at startup rather than silently serving plaintext); terminate REST TLS with a reverse proxy.

When deploying in environments where only local access is required, administrators can configure the server to bind exclusively to localhost addresses. This can be achieved by setting the bind address to `127.0.0.1` for IPv4 or `::1` for IPv6, which restricts incoming connections to the local machine only. This configuration prevents external network access to the server endpoints, providing an additional layer of security for local development or testing environments.
```
Expand Down
78 changes: 78 additions & 0 deletions src/capi_frontend/capi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,84 @@ DLL_PUBLIC OVMS_Status* OVMS_ServerSettingsSetAllowedMediaDomains(OVMS_ServerSet
return nullptr;
}

DLL_PUBLIC OVMS_Status* OVMS_ServerSettingsSetGrpcCertPath(OVMS_ServerSettings* settings,
const char* grpc_cert_path) {
if (settings == nullptr) {
return reinterpret_cast<OVMS_Status*>(new Status(StatusCode::NONEXISTENT_PTR, "server settings"));
}
if (grpc_cert_path == nullptr) {
return reinterpret_cast<OVMS_Status*>(new Status(StatusCode::NONEXISTENT_PTR, "grpc certificate path"));
}
ovms::ServerSettingsImpl* serverSettings = reinterpret_cast<ovms::ServerSettingsImpl*>(settings);
serverSettings->grpcCertPath.assign(grpc_cert_path);
return nullptr;
}

DLL_PUBLIC OVMS_Status* OVMS_ServerSettingsSetGrpcKeyPath(OVMS_ServerSettings* settings,
const char* grpc_key_path) {
if (settings == nullptr) {
return reinterpret_cast<OVMS_Status*>(new Status(StatusCode::NONEXISTENT_PTR, "server settings"));
}
if (grpc_key_path == nullptr) {
return reinterpret_cast<OVMS_Status*>(new Status(StatusCode::NONEXISTENT_PTR, "grpc key path"));
}
ovms::ServerSettingsImpl* serverSettings = reinterpret_cast<ovms::ServerSettingsImpl*>(settings);
serverSettings->grpcKeyPath.assign(grpc_key_path);
return nullptr;
}

DLL_PUBLIC OVMS_Status* OVMS_ServerSettingsSetGrpcCaPath(OVMS_ServerSettings* settings,
const char* grpc_ca_path) {
if (settings == nullptr) {
return reinterpret_cast<OVMS_Status*>(new Status(StatusCode::NONEXISTENT_PTR, "server settings"));
}
if (grpc_ca_path == nullptr) {
return reinterpret_cast<OVMS_Status*>(new Status(StatusCode::NONEXISTENT_PTR, "grpc CA path"));
}
ovms::ServerSettingsImpl* serverSettings = reinterpret_cast<ovms::ServerSettingsImpl*>(settings);
serverSettings->grpcCaPath.assign(grpc_ca_path);
return nullptr;
}

DLL_PUBLIC OVMS_Status* OVMS_ServerSettingsSetRestCertPath(OVMS_ServerSettings* settings,
const char* rest_cert_path) {
if (settings == nullptr) {
return reinterpret_cast<OVMS_Status*>(new Status(StatusCode::NONEXISTENT_PTR, "server settings"));
}
if (rest_cert_path == nullptr) {
return reinterpret_cast<OVMS_Status*>(new Status(StatusCode::NONEXISTENT_PTR, "rest certificate path"));
}
ovms::ServerSettingsImpl* serverSettings = reinterpret_cast<ovms::ServerSettingsImpl*>(settings);
serverSettings->restCertPath.assign(rest_cert_path);
return nullptr;
}

DLL_PUBLIC OVMS_Status* OVMS_ServerSettingsSetRestKeyPath(OVMS_ServerSettings* settings,
const char* rest_key_path) {
if (settings == nullptr) {
return reinterpret_cast<OVMS_Status*>(new Status(StatusCode::NONEXISTENT_PTR, "server settings"));
}
if (rest_key_path == nullptr) {
return reinterpret_cast<OVMS_Status*>(new Status(StatusCode::NONEXISTENT_PTR, "rest key path"));
}
ovms::ServerSettingsImpl* serverSettings = reinterpret_cast<ovms::ServerSettingsImpl*>(settings);
serverSettings->restKeyPath.assign(rest_key_path);
return nullptr;
}

DLL_PUBLIC OVMS_Status* OVMS_ServerSettingsSetRestCaPath(OVMS_ServerSettings* settings,
const char* rest_ca_path) {
if (settings == nullptr) {
return reinterpret_cast<OVMS_Status*>(new Status(StatusCode::NONEXISTENT_PTR, "server settings"));
}
if (rest_ca_path == nullptr) {
return reinterpret_cast<OVMS_Status*>(new Status(StatusCode::NONEXISTENT_PTR, "rest CA path"));
}
ovms::ServerSettingsImpl* serverSettings = reinterpret_cast<ovms::ServerSettingsImpl*>(settings);
serverSettings->restCaPath.assign(rest_ca_path);
return nullptr;
}

DLL_PUBLIC OVMS_Status* OVMS_ModelsSettingsSetConfigPath(OVMS_ModelsSettings* settings,
const char* config_path) {
if (settings == nullptr) {
Expand Down
6 changes: 6 additions & 0 deletions src/capi_frontend/server_settings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ struct ServerSettingsImpl {
uint32_t filesystemPollWaitMilliseconds = 1000;
uint32_t resourcesCleanerPollWaitSeconds = 300;
std::string cacheDir;
std::string grpcCertPath;
std::string grpcKeyPath;
std::string grpcCaPath;
std::string restCertPath;
std::string restKeyPath;
std::string restCaPath;
bool withPython = false;
bool startedWithCLI = false;
ConfigExportType exportConfigType = UNKNOWN_MODEL;
Expand Down
39 changes: 38 additions & 1 deletion src/cli_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,31 @@ std::variant<bool, std::pair<int, std::string>> CLIParser::parse(int argc, char*
("api_key_file",
"path to the text file containing API key for authentication for generative endpoints. If not set, authentication is disabled.",
cxxopts::value<std::string>()->default_value(""),
"API_KEY");
"API_KEY")
("grpc_certificate_path",
"Path to the PEM-encoded server certificate for gRPC TLS. Must be set together with grpc_key_path to enable TLS.",
cxxopts::value<std::string>(),
"GRPC_CERTIFICATE_PATH")
("grpc_key_path",
"Path to the PEM-encoded private key for gRPC TLS. Must be set together with grpc_certificate_path to enable TLS.",
cxxopts::value<std::string>(),
"GRPC_KEY_PATH")
("grpc_ca_path",
"Path to the PEM-encoded CA certificate for gRPC mutual TLS (mTLS). Requires grpc_certificate_path and grpc_key_path. When set, client certificates are required and verified.",
cxxopts::value<std::string>(),
"GRPC_CA_PATH")
("rest_certificate_path",
"Path to the PEM-encoded server certificate for REST TLS (HTTPS). Must be set together with rest_key_path to enable TLS.",
cxxopts::value<std::string>(),
"REST_CERTIFICATE_PATH")
("rest_key_path",
"Path to the PEM-encoded private key for REST TLS (HTTPS). Must be set together with rest_certificate_path to enable TLS.",
cxxopts::value<std::string>(),
"REST_KEY_PATH")
("rest_ca_path",
"Path to the PEM-encoded CA certificate for REST client-certificate verification. Requires rest_certificate_path and rest_key_path. Note: mTLS for REST requires Drogon TLS support.",
cxxopts::value<std::string>(),
"REST_CA_PATH");

options->add_options("multi model")
("config_path",
Expand Down Expand Up @@ -532,6 +556,19 @@ void CLIParser::prepareServer(ServerSettingsImpl& serverSettings) {
if (result->count("rest_bind_address"))
serverSettings.restBindAddress = result->operator[]("rest_bind_address").as<std::string>();

if (result->count("grpc_certificate_path"))
serverSettings.grpcCertPath = result->operator[]("grpc_certificate_path").as<std::string>();
if (result->count("grpc_key_path"))
serverSettings.grpcKeyPath = result->operator[]("grpc_key_path").as<std::string>();
if (result->count("grpc_ca_path"))
serverSettings.grpcCaPath = result->operator[]("grpc_ca_path").as<std::string>();
if (result->count("rest_certificate_path"))
serverSettings.restCertPath = result->operator[]("rest_certificate_path").as<std::string>();
if (result->count("rest_key_path"))
serverSettings.restKeyPath = result->operator[]("rest_key_path").as<std::string>();
if (result->count("rest_ca_path"))
serverSettings.restCaPath = result->operator[]("rest_ca_path").as<std::string>();

if (result->count("grpc_max_threads"))
serverSettings.grpcMaxThreads = result->operator[]("grpc_max_threads").as<uint32_t>();

Expand Down
62 changes: 62 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,44 @@ bool Config::validateUserSettingsInConfigAddRemoveModel(const ModelsSettingsImpl
return true;
}

// Validates a set of TLS material paths (certificate, private key, optional CA) for one
// endpoint. cert and key must be set together; each provided file must exist and be
// non-empty (an empty/truncated cert or key would otherwise pass and later cause an
// opaque bind/handshake failure); a CA requires cert+key. flagPrefix is the user-facing
// option prefix, e.g. "grpc" or "rest", used only in error messages.
static bool validateTlsMaterial(const std::string& certPath, const std::string& keyPath, const std::string& caPath, const std::string& flagPrefix) {
auto fileUsable = [](const std::string& p) {
std::error_code ec;
return std::filesystem::exists(p) && std::filesystem::file_size(p, ec) > 0 && !ec;
};
const bool hasCert = !certPath.empty();
const bool hasKey = !keyPath.empty();
const bool hasCa = !caPath.empty();
if (hasCert != hasKey) {
std::cerr << flagPrefix << "_certificate_path and " << flagPrefix << "_key_path must both be set to enable TLS" << std::endl;
return false;
}
if (hasCert && !fileUsable(certPath)) {
std::cerr << "File path provided as --" << flagPrefix << "_certificate_path does not exist or is empty: " << certPath << std::endl;
return false;
}
if (hasKey && !fileUsable(keyPath)) {
std::cerr << "File path provided as --" << flagPrefix << "_key_path does not exist or is empty: " << keyPath << std::endl;
return false;
}
if (hasCa) {
if (!hasCert) {
std::cerr << flagPrefix << "_ca_path requires " << flagPrefix << "_certificate_path and " << flagPrefix << "_key_path to be set" << std::endl;
return false;
}
if (!fileUsable(caPath)) {
std::cerr << "File path provided as --" << flagPrefix << "_ca_path does not exist or is empty: " << caPath << std::endl;
return false;
}
}
return true;
}

bool Config::validate() {
if (!this->serverSettings.hfSettings.sourceModel.empty() && this->serverSettings.hfSettings.task == UNKNOWN_GRAPH) {
std::cerr << "--source_model should be used combined with --task" << std::endl;
Expand Down Expand Up @@ -352,6 +390,24 @@ bool Config::validate() {
return false;
}

// gRPC TLS: validate cert/key/CA paths.
if (!validateTlsMaterial(this->serverSettings.grpcCertPath, this->serverSettings.grpcKeyPath, this->serverSettings.grpcCaPath, "grpc")) {
return false;
}

// REST TLS is gated: the bundled Drogon is currently built without OpenSSL, so it
// cannot serve HTTPS (enabling SSL would silently fall back to plaintext). Fail
// closed rather than expose a plaintext endpoint a user believes is encrypted.
// The REST TLS wiring (addListener SSL, see drogon_http_server.cpp) is in place and
// will activate once Drogon is built with OpenSSL. To enable REST TLS at that point,
// replace this guard with:
// if (!validateTlsMaterial(restCertPath, restKeyPath, restCaPath, "rest")) return false;
// Use gRPC TLS or a TLS-terminating proxy for REST in the meantime. See issue #2144.
if (!this->serverSettings.restCertPath.empty() || !this->serverSettings.restKeyPath.empty() || !this->serverSettings.restCaPath.empty()) {
std::cerr << "REST TLS (rest_certificate_path/rest_key_path/rest_ca_path) is not supported in this build because the bundled Drogon was built without OpenSSL. Use gRPC TLS (grpc_certificate_path/grpc_key_path) or terminate REST TLS with a proxy." << std::endl;
return false;
}

// check bind addresses:
if (!restBindAddress().empty() && check_hostname_or_ip(restBindAddress()) == false) {
std::cerr << "rest_bind_address has invalid format: proper hostname or IP address expected." << std::endl;
Expand Down Expand Up @@ -427,5 +483,11 @@ const std::string& Config::allowedMethods() const { return this->serverSettings.
const std::string& Config::allowedHeaders() const { return this->serverSettings.allowedHeaders; }
const std::string Config::cacheDir() const { return this->serverSettings.cacheDir; }
const std::string& Config::apiKey() const { return this->serverSettings.apiKey; }
const std::string Config::grpcCertPath() const { return this->serverSettings.grpcCertPath; }
const std::string Config::grpcKeyPath() const { return this->serverSettings.grpcKeyPath; }
const std::string Config::grpcCaPath() const { return this->serverSettings.grpcCaPath; }
const std::string Config::restCertPath() const { return this->serverSettings.restCertPath; }
const std::string Config::restKeyPath() const { return this->serverSettings.restKeyPath; }
const std::string Config::restCaPath() const { return this->serverSettings.restCaPath; }

} // namespace ovms
9 changes: 9 additions & 0 deletions src/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,15 @@ class Config {
* @return const std::string&
*/
const std::string cacheDir() const;

// TLS configuration accessors
const std::string grpcCertPath() const;
const std::string grpcKeyPath() const;
const std::string grpcCaPath() const;
const std::string restCertPath() const;
const std::string restKeyPath() const;
const std::string restCaPath() const;

bool startedFromCLI() {
return serverSettings.startedWithCLI;
}
Expand Down
26 changes: 23 additions & 3 deletions src/drogon_http_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@

namespace ovms {

DrogonHttpServer::DrogonHttpServer(size_t numWorkersForUnary, size_t numWorkersForStreaming, int port, const std::string& address) :
DrogonHttpServer::DrogonHttpServer(size_t numWorkersForUnary, size_t numWorkersForStreaming, int port, const std::string& address, const std::string& certPath, const std::string& keyPath, const std::string& caPath) :
numWorkersForUnary(numWorkersForUnary),
numWorkersForStreaming(numWorkersForStreaming),
pool(std::make_unique<mediapipe::ThreadPool>("DrogonThreadPool", numWorkersForStreaming)),
port(port),
address(address) {
address(address),
certPath(certPath),
keyPath(keyPath),
caPath(caPath) {
SPDLOG_DEBUG("Starting http thread pool for streaming ({} threads)", numWorkersForStreaming);
pool->StartWorkers(); // this tp is for streaming workload which cannot use drogon's internal listener threads
SPDLOG_DEBUG("Thread pool started");
Expand Down Expand Up @@ -153,9 +156,26 @@ Status DrogonHttpServer::startAcceptingRequests() {
});

auto ips = ovms::tokenize(this->address, ',');
const bool useTls = !this->certPath.empty() && !this->keyPath.empty();
if (useTls) {
if (!this->caPath.empty()) {
SPDLOG_INFO("REST TLS enabled with client certificate verification (mTLS)");
} else {
SPDLOG_INFO("REST TLS enabled (server-only TLS, no client certificate verification)");
}
}
for (const auto& ip : ips) {
SPDLOG_INFO("Binding REST server to address: {}:{}", ip, this->port);
drogon::app().addListener(ip, this->port);
if (useTls) {
std::vector<std::pair<std::string, std::string>> sslConfCmds;
if (!this->caPath.empty()) {
sslConfCmds.push_back({"CAfile", this->caPath});
sslConfCmds.push_back({"VerifyPeer", "1"});
}
drogon::app().addListener(ip, this->port, /*useSSL=*/true, this->certPath, this->keyPath, /*useOldTLS=*/false, sslConfCmds);
} else {
drogon::app().addListener(ip, this->port);
}
}
drogon::app().run();
} catch (...) {
Expand Down
Loading