admin api: add metrics_require_token config option and update doc
This commit is contained in:
parent
004eb94e14
commit
ff6ec62d54
3 changed files with 97 additions and 79 deletions
|
@ -80,6 +80,7 @@ add_host_to_metrics = true
|
||||||
[admin]
|
[admin]
|
||||||
api_bind_addr = "0.0.0.0:3903"
|
api_bind_addr = "0.0.0.0:3903"
|
||||||
metrics_token = "BCAdFjoa9G0KJR0WXnHHm7fs1ZAbfpI8iIZ+Z/a2NgI="
|
metrics_token = "BCAdFjoa9G0KJR0WXnHHm7fs1ZAbfpI8iIZ+Z/a2NgI="
|
||||||
|
metrics_require_token = true
|
||||||
admin_token = "UkLeGWEvHnXBqnueR3ISEMWpOnm40jH2tM2HnnL/0F4="
|
admin_token = "UkLeGWEvHnXBqnueR3ISEMWpOnm40jH2tM2HnnL/0F4="
|
||||||
trace_sink = "http://localhost:4317"
|
trace_sink = "http://localhost:4317"
|
||||||
```
|
```
|
||||||
|
@ -145,6 +146,7 @@ The `[s3_web]` section:
|
||||||
|
|
||||||
The `[admin]` section:
|
The `[admin]` section:
|
||||||
[`api_bind_addr`](#admin_api_bind_addr),
|
[`api_bind_addr`](#admin_api_bind_addr),
|
||||||
|
[`metrics_require_token`](#admin_metrics_require_token),
|
||||||
[`metrics_token`/`metrics_token_file`](#admin_metrics_token),
|
[`metrics_token`/`metrics_token_file`](#admin_metrics_token),
|
||||||
[`admin_token`/`admin_token_file`](#admin_token),
|
[`admin_token`/`admin_token_file`](#admin_token),
|
||||||
[`trace_sink`](#admin_trace_sink),
|
[`trace_sink`](#admin_trace_sink),
|
||||||
|
@ -767,10 +769,34 @@ See [administration API reference](@/documentation/reference-manual/admin-api.md
|
||||||
Alternatively, since `v0.8.5`, a path can be used to create a unix socket. Note that for security reasons,
|
Alternatively, since `v0.8.5`, a path can be used to create a unix socket. Note that for security reasons,
|
||||||
the socket will have 0220 mode. Make sure to set user and group permissions accordingly.
|
the socket will have 0220 mode. Make sure to set user and group permissions accordingly.
|
||||||
|
|
||||||
|
#### `admin_token`, `admin_token_file` or `GARAGE_ADMIN_TOKEN`, `GARAGE_ADMIN_TOKEN_FILE` (env) {#admin_token}
|
||||||
|
|
||||||
|
The token for accessing all administration functions on the admin endpoint,
|
||||||
|
with the exception of the metrics endpoint (see `metrics_token`).
|
||||||
|
|
||||||
|
You can use any random string for this value. We recommend generating a random
|
||||||
|
token with `openssl rand -base64 32`.
|
||||||
|
|
||||||
|
For Garage version earlier than `v2.0`, if this token is not set,
|
||||||
|
access to these endpoints is disabled entirely.
|
||||||
|
|
||||||
|
Since Garage `v2.0`, additional admin API tokens can be defined dynamically
|
||||||
|
in your Garage cluster using administration commands. This new admin token system
|
||||||
|
is more flexible since it allows admin tokens to have an expiration date,
|
||||||
|
and to have a scope restricted to certain admin API functions. If `admin_token`
|
||||||
|
is set, it behaves as an admin token without expiration and with full scope.
|
||||||
|
Otherwise, only admin API tokens defined dynamically can be used.
|
||||||
|
|
||||||
|
`admin_token` was introduced in Garage `v0.7.2`.
|
||||||
|
`admin_token_file` and the `GARAGE_ADMIN_TOKEN` environment variable are supported since Garage `v0.8.2`.
|
||||||
|
|
||||||
|
`GARAGE_ADMIN_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
||||||
|
|
||||||
#### `metrics_token`, `metrics_token_file` or `GARAGE_METRICS_TOKEN`, `GARAGE_METRICS_TOKEN_FILE` (env) {#admin_metrics_token}
|
#### `metrics_token`, `metrics_token_file` or `GARAGE_METRICS_TOKEN`, `GARAGE_METRICS_TOKEN_FILE` (env) {#admin_metrics_token}
|
||||||
|
|
||||||
The token for accessing the Metrics endpoint. If this token is not set, the
|
The token for accessing the Prometheus metrics endpoint (`/metrics`).
|
||||||
Metrics endpoint can be accessed without access control.
|
If this token is not set, and unless `metrics_require_token` is set to `true`,
|
||||||
|
the metrics endpoint can be accessed without access control.
|
||||||
|
|
||||||
You can use any random string for this value. We recommend generating a random token with `openssl rand -base64 32`.
|
You can use any random string for this value. We recommend generating a random token with `openssl rand -base64 32`.
|
||||||
|
|
||||||
|
@ -779,17 +805,12 @@ You can use any random string for this value. We recommend generating a random t
|
||||||
|
|
||||||
`GARAGE_METRICS_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
`GARAGE_METRICS_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
||||||
|
|
||||||
#### `admin_token`, `admin_token_file` or `GARAGE_ADMIN_TOKEN`, `GARAGE_ADMIN_TOKEN_FILE` (env) {#admin_token}
|
#### `metrics_require_token` (since `v2.0.0`) {#admin_metrics_require_token}
|
||||||
|
|
||||||
The token for accessing all of the other administration endpoints. If this
|
If this is set to `true`, accessing the metrics endpoint will always require
|
||||||
token is not set, access to these endpoints is disabled entirely.
|
an access token. Valid tokens include the `metrics_token` if it is set,
|
||||||
|
and admin API token defined dynamicaly in Garage which have
|
||||||
You can use any random string for this value. We recommend generating a random token with `openssl rand -base64 32`.
|
the `Metrics` endpoint in their scope.
|
||||||
|
|
||||||
`admin_token` was introduced in Garage `v0.7.2`.
|
|
||||||
`admin_token_file` and the `GARAGE_ADMIN_TOKEN` environment variable are supported since Garage `v0.8.2`.
|
|
||||||
|
|
||||||
`GARAGE_ADMIN_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
|
||||||
|
|
||||||
#### `trace_sink` {#admin_trace_sink}
|
#### `trace_sink` {#admin_trace_sink}
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,7 @@ pub struct AdminApiServer {
|
||||||
#[cfg(feature = "metrics")]
|
#[cfg(feature = "metrics")]
|
||||||
pub(crate) exporter: PrometheusExporter,
|
pub(crate) exporter: PrometheusExporter,
|
||||||
metrics_token: Option<String>,
|
metrics_token: Option<String>,
|
||||||
|
metrics_require_token: bool,
|
||||||
admin_token: Option<String>,
|
admin_token: Option<String>,
|
||||||
pub(crate) background: Arc<BackgroundRunner>,
|
pub(crate) background: Arc<BackgroundRunner>,
|
||||||
pub(crate) endpoint: Arc<RpcEndpoint<AdminRpc, Self>>,
|
pub(crate) endpoint: Arc<RpcEndpoint<AdminRpc, Self>>,
|
||||||
|
@ -118,6 +119,7 @@ impl AdminApiServer {
|
||||||
let cfg = &garage.config.admin;
|
let cfg = &garage.config.admin;
|
||||||
let metrics_token = cfg.metrics_token.as_deref().map(hash_bearer_token);
|
let metrics_token = cfg.metrics_token.as_deref().map(hash_bearer_token);
|
||||||
let admin_token = cfg.admin_token.as_deref().map(hash_bearer_token);
|
let admin_token = cfg.admin_token.as_deref().map(hash_bearer_token);
|
||||||
|
let metrics_require_token = cfg.metrics_require_token;
|
||||||
|
|
||||||
let endpoint = garage.system.netapp.endpoint(ADMIN_RPC_PATH.into());
|
let endpoint = garage.system.netapp.endpoint(ADMIN_RPC_PATH.into());
|
||||||
let admin = Arc::new(Self {
|
let admin = Arc::new(Self {
|
||||||
|
@ -125,6 +127,7 @@ impl AdminApiServer {
|
||||||
#[cfg(feature = "metrics")]
|
#[cfg(feature = "metrics")]
|
||||||
exporter,
|
exporter,
|
||||||
metrics_token,
|
metrics_token,
|
||||||
|
metrics_require_token,
|
||||||
admin_token,
|
admin_token,
|
||||||
background,
|
background,
|
||||||
endpoint,
|
endpoint,
|
||||||
|
@ -156,25 +159,19 @@ impl AdminApiServer {
|
||||||
HttpEndpoint::New(_) => AdminApiRequest::from_request(req).await?,
|
HttpEndpoint::New(_) => AdminApiRequest::from_request(req).await?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let required_auth_hash =
|
let (global_token_hash, token_required) = match request.authorization_type() {
|
||||||
match request.authorization_type() {
|
Authorization::None => (None, false),
|
||||||
Authorization::None => None,
|
Authorization::MetricsToken => (
|
||||||
Authorization::MetricsToken => self.metrics_token.as_deref(),
|
self.metrics_token.as_deref(),
|
||||||
Authorization::AdminToken => match self.admin_token.as_deref() {
|
self.metrics_token.is_some() || self.metrics_require_token,
|
||||||
None => return Err(Error::forbidden(
|
),
|
||||||
"Admin token isn't configured, admin API access is disabled for security.",
|
Authorization::AdminToken => (self.admin_token.as_deref(), true),
|
||||||
)),
|
};
|
||||||
Some(t) => Some(t),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
verify_authorization(
|
if token_required {
|
||||||
&self.garage,
|
verify_authorization(&self.garage, global_token_hash, auth_header, request.name())
|
||||||
required_auth_hash,
|
.await?;
|
||||||
auth_header,
|
}
|
||||||
request.name(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
match request {
|
match request {
|
||||||
AdminApiRequest::Options(req) => req.handle(&self.garage, &self).await,
|
AdminApiRequest::Options(req) => req.handle(&self.garage, &self).await,
|
||||||
|
@ -250,7 +247,7 @@ fn hash_bearer_token(token: &str) -> String {
|
||||||
|
|
||||||
async fn verify_authorization(
|
async fn verify_authorization(
|
||||||
garage: &Garage,
|
garage: &Garage,
|
||||||
required_token_hash: Option<&str>,
|
global_token_hash: Option<&str>,
|
||||||
auth_header: Option<hyper::http::HeaderValue>,
|
auth_header: Option<hyper::http::HeaderValue>,
|
||||||
endpoint_name: &str,
|
endpoint_name: &str,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
@ -258,55 +255,52 @@ async fn verify_authorization(
|
||||||
|
|
||||||
let invalid_msg = "Invalid bearer token";
|
let invalid_msg = "Invalid bearer token";
|
||||||
|
|
||||||
if let Some(token_hash_str) = required_token_hash {
|
let token = match &auth_header {
|
||||||
let token = match &auth_header {
|
None => {
|
||||||
None => {
|
return Err(Error::forbidden(
|
||||||
return Err(Error::forbidden(
|
"Bearer token must be provided in Authorization header",
|
||||||
"Bearer token must be provided in Authorization header",
|
))
|
||||||
))
|
|
||||||
}
|
|
||||||
Some(authorization) => authorization
|
|
||||||
.to_str()?
|
|
||||||
.strip_prefix("Bearer ")
|
|
||||||
.ok_or_else(|| Error::forbidden("Invalid Authorization header"))?
|
|
||||||
.trim(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let token_hash_string = if let Some((prefix, _)) = token.split_once('.') {
|
|
||||||
garage
|
|
||||||
.admin_token_table
|
|
||||||
.get(&EmptyKey, &prefix.to_string())
|
|
||||||
.await?
|
|
||||||
.and_then(|k| k.state.into_option())
|
|
||||||
.filter(|p| {
|
|
||||||
p.expiration
|
|
||||||
.get()
|
|
||||||
.map(|exp| now_msec() < exp)
|
|
||||||
.unwrap_or(true)
|
|
||||||
})
|
|
||||||
.filter(|p| {
|
|
||||||
p.scope
|
|
||||||
.get()
|
|
||||||
.0
|
|
||||||
.iter()
|
|
||||||
.any(|x| x == "*" || x == endpoint_name)
|
|
||||||
})
|
|
||||||
.ok_or_else(|| Error::forbidden(invalid_msg))?
|
|
||||||
.token_hash
|
|
||||||
} else {
|
|
||||||
token_hash_str.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let token_hash = PasswordHash::new(&token_hash_string)
|
|
||||||
.ok_or_internal_error("Could not parse token hash")?;
|
|
||||||
|
|
||||||
if Argon2::default()
|
|
||||||
.verify_password(token.as_bytes(), &token_hash)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return Err(Error::forbidden(invalid_msg));
|
|
||||||
}
|
}
|
||||||
}
|
Some(authorization) => authorization
|
||||||
|
.to_str()?
|
||||||
|
.strip_prefix("Bearer ")
|
||||||
|
.ok_or_else(|| Error::forbidden("Invalid Authorization header"))?
|
||||||
|
.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let token_hash_string = if let Some((prefix, _)) = token.split_once('.') {
|
||||||
|
garage
|
||||||
|
.admin_token_table
|
||||||
|
.get(&EmptyKey, &prefix.to_string())
|
||||||
|
.await?
|
||||||
|
.and_then(|k| k.state.into_option())
|
||||||
|
.filter(|p| {
|
||||||
|
p.expiration
|
||||||
|
.get()
|
||||||
|
.map(|exp| now_msec() < exp)
|
||||||
|
.unwrap_or(true)
|
||||||
|
})
|
||||||
|
.filter(|p| {
|
||||||
|
p.scope
|
||||||
|
.get()
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.any(|x| x == "*" || x == endpoint_name)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| Error::forbidden(invalid_msg))?
|
||||||
|
.token_hash
|
||||||
|
} else {
|
||||||
|
global_token_hash
|
||||||
|
.ok_or_else(|| Error::forbidden(invalid_msg))?
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let token_hash =
|
||||||
|
PasswordHash::new(&token_hash_string).ok_or_internal_error("Could not parse token hash")?;
|
||||||
|
|
||||||
|
Argon2::default()
|
||||||
|
.verify_password(token.as_bytes(), &token_hash)
|
||||||
|
.map_err(|_| Error::forbidden(invalid_msg))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -198,6 +198,9 @@ pub struct AdminConfig {
|
||||||
pub metrics_token: Option<String>,
|
pub metrics_token: Option<String>,
|
||||||
/// File to read metrics token from
|
/// File to read metrics token from
|
||||||
pub metrics_token_file: Option<PathBuf>,
|
pub metrics_token_file: Option<PathBuf>,
|
||||||
|
/// Whether to require an access token for accessing the metrics endpoint
|
||||||
|
#[serde(default)]
|
||||||
|
pub metrics_require_token: bool,
|
||||||
|
|
||||||
/// Bearer token to use to access Admin API endpoints
|
/// Bearer token to use to access Admin API endpoints
|
||||||
pub admin_token: Option<String>,
|
pub admin_token: Option<String>,
|
||||||
|
|
Loading…
Add table
Reference in a new issue