Skip to content

Commit 4236db5

Browse files
dtrawinsatobiszeimzegla
authored
add api-key parameter (#3690)
### 🛠 Summary CVS-174477 Enable client authorization using API key passed in authorization header. API key can be set using --api_key_file parameter or using API_KEY env variable Key verification is enabled only for generative endpoints /v3 ### 🧪 Checklist - [x] Unit tests added. - [x] The documentation updated. - [x] Change follows security best practices. `` --------- Co-authored-by: Adrian Tobiszewski <adrian.tobiszewski@intel.com> Co-authored-by: Miłosz Żeglarski <milosz.zeglarski@intel.com>
1 parent ea50269 commit 4236db5

26 files changed

+323
-266
lines changed

ci/loadWin.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def install_dependencies() {
111111
def clean() {
112112
def output1 = bat(returnStdout: true, script: 'windows_clean_build.bat ' + get_short_bazel_path() + ' ' + env.OVMS_CLEAN_EXPUNGE)
113113
if(fileExists('dist\\windows\\ovms')){
114-
def status_del = bat(returnStatus: true, script: 'rmdir /s /q ovms')
114+
def status_del = bat(returnStatus: true, script: 'rmdir /s /q dist\\windows\\ovms')
115115
if (status_del != 0) {
116116
error "Error: Deleting existing ovms directory failed ${status_del}. Check pipeline.log for details."
117117
} else {

docs/parameters.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Configuration options for the server are defined only via command-line options a
5656
| `allowed_headers` | `string` (default: *) | Comma-separated list of allowed headers in CORS requests. |
5757
| `allowed_methods` | `string` (default: *) | Comma-separated list of allowed methods in CORS requests. |
5858
| `allowed_origins` | `string` (default: *) | Comma-separated list of allowed origins in CORS requests. |
59+
| `api_key_file` | `string` | Path to the text file with the API key for generative endpoints `/v3/`. The value of first line is used. If not specified, server is using environment variable API_KEY. If not set, requests will not require authorization.|
5960

6061
## Config management mode options
6162

docs/security_considerations.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@
55
By default, the OpenVINO Model Server containers start with the security context of a local account `ovms` with Linux UID 5000. This ensures the Docker container does not have elevated permissions on the host machine. This is in line with best practices to use minimal permissions when running containerized applications. You can change the security context by adding the `--user` parameter to the Docker run command. This may be needed for loading mounted models with restricted access.
66
For additional security hardening, you might also consider preventing write operations on the container root filesystem by adding a `--read-only` flag. This prevents undesired modification of the container files. In case the cloud storage used for the model repository (S3, Google Storage, or Azure storage) is restricting the root filesystem, it should be combined with `--tmpfs /tmp` flag.
77

8-
```bash
9-
mkdir -p models/resnet/1
10-
wget -P models/resnet/1 https://storage.openvinotoolkit.org/repositories/open_model_zoo/2022.1/models_bin/2/resnet50-binary-0001/FP32-INT1/resnet50-binary-0001.bin
11-
wget -P models/resnet/1 https://storage.openvinotoolkit.org/repositories/open_model_zoo/2022.1/models_bin/2/resnet50-binary-0001/FP32-INT1/resnet50-binary-0001.xml
12-
13-
docker run --rm -d --user $(id -u):$(id -g) --read-only --tmpfs /tmp -v ${PWD}/models/:/models -p 9178:9178 openvino/model_server:latest \
14-
--model_path /models/resnet/ --model_name resnet --port 9178
8+
```
9+
docker run --rm -d --user $(id -u):$(id -g) --read-only --tmpfs /tmp -p 9000:9000 openvino/model_server:latest \
10+
--model_path s3://bucket/model --model_name model --port 9000
1511
1612
```
1713
---
@@ -21,11 +17,16 @@ See also:
2117
- [Securing OVMS with NGINX](../extras/nginx-mtls-auth/README.md)
2218
- [Securing models with OVSA](https://docs.openvino.ai/2025/about-openvino/openvino-ecosystem/openvino-project/openvino-security-add-on.html)
2319

20+
---
21+
Generative endpoints starting with `/v3`, might be restricted with authorization and API key. It can be set during the server initialization with a parameter `api_key_file` or environment variable `API_KEY`.
22+
The `api_key_file` should contain a path to the file containing the value of API key. The content of the file first line is used. If parameter api_key_file and variable API_KEY are not set, the server will not require any authorization. The client should send the API key inside the `Authorization` header as `Bearer <api_key>`.
23+
2424
---
2525

2626
OpenVINO Model Server has a set of mechanisms preventing denial of service attacks from the client applications. They include the following:
2727
- setting the number of inference execution streams which can limit the number of parallel inference calls in progress for each model. It can be tuned with `NUM_STREAMS` or `PERFORMANCE_HINT` plugin config.
2828
- setting the maximum number of gRPC threads which is, by default, configured to the number 8 * number_of_cores. It can be changed with the parameter `--grpc_max_threads`.
29+
- setting the maximum number of REST workers which is, be default, configured to the number 4 * number_of_cores. It can be changed with the parameter `--rest_workers`.
2930
- maximum size of REST and GRPC message which is 1GB - bigger messages will be rejected
3031
- setting max_concurrent_streams which defines how many concurrent threads can be initiated from a single client - the remaining will be queued. The default is equal to the number of CPU cores. It can be changed with the `--grpc_channel_arguments grpc.max_concurrent_streams=8`.
3132
- setting the gRPC memory quota for the requests buffer - the default is 2GB. It can be changed with `--grpc_memory_quota=2147483648`. Value `0` invalidates the quota.

src/capi_frontend/server_settings.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ struct ServerSettingsImpl {
178178
std::string allowedOrigins{"*"};
179179
std::string allowedMethods{"*"};
180180
std::string allowedHeaders{"*"};
181+
std::string apiKey;
181182
#ifdef MTR_ENABLED
182183
std::string tracePath;
183184
#endif

src/cli_parser.cpp

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
//*****************************************************************************
1616
#include "cli_parser.hpp"
1717

18+
#include <filesystem>
1819
#include <iostream>
1920
#include <stdexcept>
2021
#include <string>
@@ -35,6 +36,7 @@
3536
namespace ovms {
3637

3738
constexpr const char* CONFIG_MANAGEMENT_HELP_GROUP{"config management"};
39+
constexpr const char* API_KEY_ENV_VAR{"API_KEY"};
3840

3941
std::string getConfigPath(const std::string& configPath) {
4042
bool isDir = false;
@@ -160,7 +162,11 @@ void CLIParser::parse(int argc, char** argv) {
160162
("allowed_headers",
161163
"Comma separated list of headers that are allowed to access the API. Default: *.",
162164
cxxopts::value<std::string>()->default_value("*"),
163-
"ALLOWED_HEADERS");
165+
"ALLOWED_HEADERS")
166+
("api_key_file",
167+
"path to the text file containing API key for authentication for generative endpoints. If not set, authentication is disabled.",
168+
cxxopts::value<std::string>()->default_value(""),
169+
"API_KEY");
164170

165171
options->add_options("multi model")
166172
("config_path",
@@ -493,6 +499,31 @@ void CLIParser::prepareServer(ServerSettingsImpl& serverSettings) {
493499
serverSettings.allowedOrigins = result->operator[]("allowed_origins").as<std::string>();
494500
serverSettings.allowedMethods = result->operator[]("allowed_methods").as<std::string>();
495501
serverSettings.allowedHeaders = result->operator[]("allowed_headers").as<std::string>();
502+
std::filesystem::path apiKeyFile = result->operator[]("api_key_file").as<std::string>();
503+
serverSettings.apiKey = "";
504+
if (!apiKeyFile.empty()) {
505+
std::ifstream file(apiKeyFile);
506+
if (file.is_open()) {
507+
std::getline(file, serverSettings.apiKey);
508+
// Use first line and trim whitespace characters from both ends
509+
size_t endpos = serverSettings.apiKey.find_last_not_of(" \n\r\t");
510+
if (endpos != std::string::npos) {
511+
serverSettings.apiKey = serverSettings.apiKey.substr(0, endpos + 1);
512+
}
513+
file.close();
514+
} else {
515+
std::cerr << "Error reading API key file: Unable to open file " << apiKeyFile << std::endl;
516+
exit(OVMS_EX_USAGE);
517+
}
518+
} else {
519+
const char* envApiKey = std::getenv(API_KEY_ENV_VAR);
520+
if (envApiKey != nullptr) {
521+
serverSettings.apiKey = envApiKey;
522+
}
523+
if (serverSettings.apiKey.empty()) {
524+
std::cout << "Info: API key not provided via --api_key_file or API_KEY environment variable. Authentication will be disabled." << std::endl;
525+
}
526+
}
496527
}
497528

498529
void CLIParser::prepareModel(ModelsSettingsImpl& modelsSettings, HFSettingsImpl& hfSettings) {

src/config.cpp

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,7 @@ const uint32_t WIN_MAX_GRPC_WORKERS = 1;
3737
const uint32_t MAX_PORT_NUMBER = std::numeric_limits<uint16_t>::max();
3838

3939
// For drogon, we need to minimize the number of default workers since this value is set for both: unary and streaming (making it always double)
40-
#if (USE_DROGON == 0)
41-
const uint64_t DEFAULT_REST_WORKERS = AVAILABLE_CORES * 4.0;
42-
#else
4340
const uint64_t DEFAULT_REST_WORKERS = AVAILABLE_CORES;
44-
#endif
4541
const uint32_t DEFAULT_GRPC_MAX_THREADS = AVAILABLE_CORES * 8.0;
4642
const size_t DEFAULT_GRPC_MEMORY_QUOTA = (size_t)2 * 1024 * 1024 * 1024; // 2GB
4743
const uint64_t MAX_REST_WORKERS = 10'000;
@@ -370,5 +366,6 @@ const std::string& Config::allowedOrigins() const { return this->serverSettings.
370366
const std::string& Config::allowedMethods() const { return this->serverSettings.allowedMethods; }
371367
const std::string& Config::allowedHeaders() const { return this->serverSettings.allowedHeaders; }
372368
const std::string Config::cacheDir() const { return this->serverSettings.cacheDir; }
369+
const std::string& Config::apiKey() const { return this->serverSettings.apiKey; }
373370

374371
} // namespace ovms

src/config.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ class Config {
323323
const std::string& allowedOrigins() const;
324324
const std::string& allowedMethods() const;
325325
const std::string& allowedHeaders() const;
326+
const std::string& apiKey() const;
326327

327328
/**
328329
* @brief Model cache directory

src/http_rest_api_handler.cpp

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
//*****************************************************************************
1616
#include "http_rest_api_handler.hpp"
1717

18+
#include <algorithm>
1819
#include <cctype>
1920
#include <iomanip>
2021
#include <memory>
@@ -123,7 +124,8 @@ const std::string HttpRestApiHandler::v3_RegexExp =
123124

124125
const std::string HttpRestApiHandler::metricsRegexExp = R"((.?)\/metrics(\?(.*))?)";
125126

126-
HttpRestApiHandler::HttpRestApiHandler(ovms::Server& ovmsServer, int timeout_in_ms) :
127+
HttpRestApiHandler::HttpRestApiHandler(ovms::Server& ovmsServer, int timeout_in_ms, const std::string& apiKey) :
128+
apiKey(apiKey),
127129
predictionRegex(predictionRegexExp),
128130
modelstatusRegex(modelstatusRegexExp),
129131
configReloadRegex(configReloadRegexExp),
@@ -668,14 +670,36 @@ Status HttpRestApiHandler::processListModelsRequest(std::string& response) {
668670
return StatusCode::OK;
669671
}
670672

673+
bool HttpRestApiHandler::isAuthorized(const std::unordered_map<std::string, std::string>& headers, const std::string& apiKey) {
674+
std::unordered_map<std::string, std::string> lowercaseHeaders;
675+
for (const auto& [key, value] : headers) {
676+
std::string lowercaseKey = key;
677+
std::transform(lowercaseKey.begin(), lowercaseKey.end(), lowercaseKey.begin(), ::tolower);
678+
if (lowercaseKey == "authorization") {
679+
if (value == "Bearer " + apiKey) {
680+
return true;
681+
} else {
682+
SPDLOG_DEBUG("Unauthorized request - invalid API key provided.");
683+
return false;
684+
}
685+
}
686+
}
687+
SPDLOG_DEBUG("Unauthorized request - missing API key");
688+
return false;
689+
}
690+
671691
Status HttpRestApiHandler::processV3(const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, std::shared_ptr<HttpAsyncWriter> serverReaderWriter, std::shared_ptr<MultiPartParser> multiPartParser) {
672692
#if (MEDIAPIPE_DISABLE == 0)
673693
OVMS_PROFILE_FUNCTION();
674694

675695
HttpPayload request;
676696
std::string modelName;
677697
bool streamFieldVal = false;
678-
698+
if (!this->apiKey.empty()) {
699+
if (!isAuthorized(request_components.headers, this->apiKey)) {
700+
return StatusCode::UNAUTHORIZED;
701+
}
702+
}
679703
auto status = createV3HttpPayload(uri, request_components, response, request_body, serverReaderWriter, std::move(multiPartParser), request, modelName, streamFieldVal);
680704
if (!status.ok()) {
681705
SPDLOG_DEBUG("Failed to create V3 payload: {}", status.string());

src/http_rest_api_handler.hpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class HttpRestApiHandler {
115115
*
116116
* @param timeout_in_ms
117117
*/
118-
HttpRestApiHandler(ovms::Server& ovmsServer, int timeout_in_ms);
118+
HttpRestApiHandler(ovms::Server& ovmsServer, int timeout_in_ms, const std::string& apiKey = "");
119119

120120
Status parseRequestComponents(HttpRequestComponents& components,
121121
const std::string_view http_method,
@@ -241,6 +241,8 @@ class HttpRestApiHandler {
241241
Status processV3(const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, std::shared_ptr<HttpAsyncWriter> serverReaderWriter, std::shared_ptr<MultiPartParser> multiPartParser);
242242
Status processListModelsRequest(std::string& response);
243243
Status processRetrieveModelRequest(const std::string& name, std::string& response);
244+
bool isAuthorized(const std::unordered_map<std::string, std::string>& headers, const std::string& apiKey);
245+
const std::string apiKey;
244246

245247
private:
246248
const std::regex predictionRegex;

0 commit comments

Comments
 (0)