Compare commits

..

4 Commits

Author SHA1 Message Date
Alex Auvolat 073dd715ce bump version to v2.4.0 and update api json schema 2026-09-06 19:43:04 +02:00
Alex 845db1afa1 Merge pull request 'api/s3: don't panic when a version has no blocks' (#1522) from rajsinghtech/garage:fix/multipart-empty-blocks into main-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1522
2026-09-06 07:49:47 +00:00
Raj Singh c54077af73 api/s3: don't panic when reading a version with no blocks
body_from_blocks_range indexed all_blocks[0] to size a capacity hint. A
version with zero blocks made that index panic and kill the process on a
plain GET, the read-side twin of the CompleteMultipartUpload panic at
multipart.rs:479. Fall back to the existing 1024 floor when the slice is
empty; with a non-empty slice the hint is unchanged.
2026-09-04 03:19:07 +00:00
Raj Singh f38f001bad api/s3: don't panic when final multipart version has no blocks
CompleteMultipartUpload builds the final version and then reads
`final_version.blocks.items()[0]` unconditionally. When the assembled
version ends up with an empty block list the index panics and the
process dies before it can answer the request:

    panicked at src/api/s3/multipart.rs:479:3:
    index out of bounds: the len is 0 but the index is 0

Use `.first()` and return an internal error instead, matching how the
rest of this file handles missing data. The empty-block case is an
internal consistency condition rather than a malformed request, so it
maps to an internal error rather than a 4xx.

The same panic was reported earlier in #1403, at multipart.rs:483 on
v2.2.0. That report was closed without a fix after the reporter moved
off consistency_mode = "dangerous".

Closes #1521
2026-09-04 00:01:32 +00:00
30 changed files with 188 additions and 113 deletions
Generated
+13 -14
View File
@@ -1513,7 +1513,7 @@ dependencies = [
[[package]] [[package]]
name = "garage" name = "garage"
version = "2.3.0" version = "2.4.0"
dependencies = [ dependencies = [
"assert-json-diff", "assert-json-diff",
"async-trait", "async-trait",
@@ -1581,7 +1581,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_api_admin" name = "garage_api_admin"
version = "2.3.0" version = "2.4.0"
dependencies = [ dependencies = [
"argon2", "argon2",
"async-trait", "async-trait",
@@ -1613,7 +1613,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_api_common" name = "garage_api_common"
version = "2.3.0" version = "2.4.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -1649,7 +1649,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_api_k2v" name = "garage_api_k2v"
version = "2.3.0" version = "2.4.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"futures", "futures",
@@ -1672,7 +1672,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_api_s3" name = "garage_api_s3"
version = "2.3.0" version = "2.4.0"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"async-compression", "async-compression",
@@ -1717,7 +1717,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_block" name = "garage_block"
version = "2.3.0" version = "2.4.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"async-compression", "async-compression",
@@ -1741,7 +1741,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_db" name = "garage_db"
version = "2.3.0" version = "2.4.0"
dependencies = [ dependencies = [
"fjall", "fjall",
"heed", "heed",
@@ -1756,7 +1756,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_model" name = "garage_model"
version = "2.3.0" version = "2.4.0"
dependencies = [ dependencies = [
"arbitrary", "arbitrary",
"argon2", "argon2",
@@ -1784,7 +1784,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_net" name = "garage_net"
version = "2.3.0" version = "2.4.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"bytes", "bytes",
@@ -1810,7 +1810,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_rpc" name = "garage_rpc"
version = "2.3.0" version = "2.4.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"async-trait", "async-trait",
@@ -1843,7 +1843,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_table" name = "garage_table"
version = "2.3.0" version = "2.4.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"async-trait", "async-trait",
@@ -1864,7 +1864,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_util" name = "garage_util"
version = "2.3.0" version = "2.4.0"
dependencies = [ dependencies = [
"arbitrary", "arbitrary",
"arc-swap", "arc-swap",
@@ -1890,7 +1890,6 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.10.9", "sha2 0.10.9",
"subtle",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"toml", "toml",
@@ -1900,7 +1899,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_web" name = "garage_web"
version = "2.3.0" version = "2.4.0"
dependencies = [ dependencies = [
"garage_api_common", "garage_api_common",
"garage_api_s3", "garage_api_s3",
+12 -13
View File
@@ -25,18 +25,18 @@ default-members = ["src/garage"]
# Internal Garage crates # Internal Garage crates
format_table = { version = "0.1.1", path = "src/format-table" } format_table = { version = "0.1.1", path = "src/format-table" }
garage_api_common = { version = "2.3.0", path = "src/api/common" } garage_api_common = { version = "2.4.0", path = "src/api/common" }
garage_api_admin = { version = "2.3.0", path = "src/api/admin" } garage_api_admin = { version = "2.4.0", path = "src/api/admin" }
garage_api_s3 = { version = "2.3.0", path = "src/api/s3" } garage_api_s3 = { version = "2.4.0", path = "src/api/s3" }
garage_api_k2v = { version = "2.3.0", path = "src/api/k2v" } garage_api_k2v = { version = "2.4.0", path = "src/api/k2v" }
garage_block = { version = "2.3.0", path = "src/block" } garage_block = { version = "2.4.0", path = "src/block" }
garage_db = { version = "2.3.0", path = "src/db", default-features = false } garage_db = { version = "2.4.0", path = "src/db", default-features = false }
garage_model = { version = "2.3.0", path = "src/model", default-features = false } garage_model = { version = "2.4.0", path = "src/model", default-features = false }
garage_net = { version = "2.3.0", path = "src/net" } garage_net = { version = "2.4.0", path = "src/net" }
garage_rpc = { version = "2.3.0", path = "src/rpc" } garage_rpc = { version = "2.4.0", path = "src/rpc" }
garage_table = { version = "2.3.0", path = "src/table" } garage_table = { version = "2.4.0", path = "src/table" }
garage_util = { version = "2.3.0", path = "src/util" } garage_util = { version = "2.4.0", path = "src/util" }
garage_web = { version = "2.3.0", path = "src/web" } garage_web = { version = "2.4.0", path = "src/web" }
k2v-client = { version = "0.0.4", path = "src/k2v-client" } k2v-client = { version = "0.0.4", path = "src/k2v-client" }
# External crates from crates.io # External crates from crates.io
@@ -76,7 +76,6 @@ pnet_datalink = "0.35"
rand = "0.9" rand = "0.9"
sha1 = "0.10" sha1 = "0.10"
sha2 = "0.10" sha2 = "0.10"
subtle = "2.6.1"
timeago = { version = "0.5", default-features = false } timeago = { version = "0.5", default-features = false }
xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] }
+109 -1
View File
@@ -12,7 +12,7 @@
"name": "AGPL-3.0", "name": "AGPL-3.0",
"identifier": "AGPL-3.0" "identifier": "AGPL-3.0"
}, },
"version": "v2.3.0" "version": "v2.4.0"
}, },
"servers": [ "servers": [
{ {
@@ -3321,10 +3321,35 @@
"dbEngine" "dbEngine"
], ],
"properties": { "properties": {
"addr": {
"type": [
"string",
"null"
],
"description": "Socket address used by other nodes to connect to this node for RPC"
},
"dataPartition": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/FreeSpaceResp",
"description": "Total and available space on the disk partition(s) containing the data\ndirectory(ies)"
}
]
},
"dbEngine": { "dbEngine": {
"type": "string", "type": "string",
"description": "database engine used for metadata" "description": "database engine used for metadata"
}, },
"draining": {
"type": [
"boolean",
"null"
],
"description": "Whether this node is part of an older layout version and is draining data."
},
"garageFeatures": { "garageFeatures": {
"type": [ "type": [
"array", "array",
@@ -3346,9 +3371,38 @@
], ],
"description": "hostname of this node" "description": "hostname of this node"
}, },
"isUp": {
"type": [
"boolean",
"null"
],
"description": "Whether this node is connected in the cluster"
},
"metadataPartition": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/FreeSpaceResp",
"description": "Total and available space on the disk partition containing the\nmetadata directory"
}
]
},
"nodeId": { "nodeId": {
"type": "string" "type": "string"
}, },
"role": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/NodeAssignedRole",
"description": "Role assigned to this node in the current cluster layout"
}
]
},
"rustVersion": { "rustVersion": {
"type": "string", "type": "string",
"description": "rustc version with which this garage release was compiled" "description": "rustc version with which this garage release was compiled"
@@ -3684,10 +3738,35 @@
"dbEngine" "dbEngine"
], ],
"properties": { "properties": {
"addr": {
"type": [
"string",
"null"
],
"description": "Socket address used by other nodes to connect to this node for RPC"
},
"dataPartition": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/FreeSpaceResp",
"description": "Total and available space on the disk partition(s) containing the data\ndirectory(ies)"
}
]
},
"dbEngine": { "dbEngine": {
"type": "string", "type": "string",
"description": "database engine used for metadata" "description": "database engine used for metadata"
}, },
"draining": {
"type": [
"boolean",
"null"
],
"description": "Whether this node is part of an older layout version and is draining data."
},
"garageFeatures": { "garageFeatures": {
"type": [ "type": [
"array", "array",
@@ -3709,9 +3788,38 @@
], ],
"description": "hostname of this node" "description": "hostname of this node"
}, },
"isUp": {
"type": [
"boolean",
"null"
],
"description": "Whether this node is connected in the cluster"
},
"metadataPartition": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/FreeSpaceResp",
"description": "Total and available space on the disk partition containing the\nmetadata directory"
}
]
},
"nodeId": { "nodeId": {
"type": "string" "type": "string"
}, },
"role": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/NodeAssignedRole",
"description": "Role assigned to this node in the current cluster layout"
}
]
},
"rustVersion": { "rustVersion": {
"type": "string", "type": "string",
"description": "rustc version with which this garage release was compiled" "description": "rustc version with which this garage release was compiled"
+5 -5
View File
@@ -96,14 +96,14 @@ to store 2 TB of data in total.
## Get a Docker image ## Get a Docker image
Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated). Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
We encourage you to use a fixed tag (eg. `v2.3.0`) and not the `latest` tag. We encourage you to use a fixed tag (eg. `v2.4.0`) and not the `latest` tag.
For this example, we will use the latest published version at the time of the writing which is `v2.3.0` but it's up to you For this example, we will use the latest published version at the time of the writing which is `v2.4.0` but it's up to you
to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated). to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
For example: For example:
``` ```
docker pull dxflrs/garage:v2.3.0 docker pull dxflrs/garage:v2.4.0
``` ```
## Deploying and configuring Garage ## Deploying and configuring Garage
@@ -171,7 +171,7 @@ docker run \
-v /etc/garage.toml:/etc/garage.toml \ -v /etc/garage.toml:/etc/garage.toml \
-v /var/lib/garage/meta:/var/lib/garage/meta \ -v /var/lib/garage/meta:/var/lib/garage/meta \
-v /var/lib/garage/data:/var/lib/garage/data \ -v /var/lib/garage/data:/var/lib/garage/data \
dxflrs/garage:v2.3.0 dxflrs/garage:v2.4.0
``` ```
With this command line, Garage should be started automatically at each boot. With this command line, Garage should be started automatically at each boot.
@@ -185,7 +185,7 @@ If you want to use `docker-compose`, you may use the following `docker-compose.y
version: "3" version: "3"
services: services:
garage: garage:
image: dxflrs/garage:v2.3.0 image: dxflrs/garage:v2.4.0
network_mode: "host" network_mode: "host"
restart: unless-stopped restart: unless-stopped
volumes: volumes:
+3 -3
View File
@@ -111,7 +111,7 @@ your data to be persisted properly.
### Configuring initial access credentials ### Configuring initial access credentials
Since `v2.3.0`, Garage can automatically create a default access key and a default storage bucket, Since `v2.n.0`, Garage can automatically create a default access key and a default storage bucket,
based on values provided in environment variables. based on values provided in environment variables.
To use this feature, export the following environment variables: To use this feature, export the following environment variables:
@@ -157,7 +157,7 @@ docker run \
-e GARAGE_DEFAULT_ACCESS_KEY \ -e GARAGE_DEFAULT_ACCESS_KEY \
-e GARAGE_DEFAULT_SECRET_KEY \ -e GARAGE_DEFAULT_SECRET_KEY \
-e GARAGE_DEFAULT_BUCKET \ -e GARAGE_DEFAULT_BUCKET \
dxflrs/garage:v2.3.0 dxflrs/garage:v2.4.0
/garage server --single-node --default-bucket /garage server --single-node --default-bucket
``` ```
@@ -337,7 +337,7 @@ When you first start a cluster without automatic configuration, the output of `g
``` ```
==== HEALTHY NODES ==== ==== HEALTHY NODES ====
ID Hostname Address Tags Zone Capacity DataAvail Version ID Hostname Address Tags Zone Capacity DataAvail Version
563e1ac825ee3323 linuxbox 127.0.0.1:3901 NO ROLE ASSIGNED v2.3.0 563e1ac825ee3323 linuxbox 127.0.0.1:3901 NO ROLE ASSIGNED v2.4.0
``` ```
Creating a cluster layout for a Garage deployment means informing Garage of the Creating a cluster layout for a Garage deployment means informing Garage of the
+2 -2
View File
@@ -2,8 +2,8 @@ apiVersion: v2
name: garage name: garage
description: S3-compatible object store for small self-hosted geo-distributed deployments description: S3-compatible object store for small self-hosted geo-distributed deployments
type: application type: application
version: 0.9.3 version: 0.9.4
appVersion: "v2.3.0" appVersion: "v2.4.0"
home: https://garagehq.deuxfleurs.fr/ home: https://garagehq.deuxfleurs.fr/
icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg
+1 -1
View File
@@ -1,6 +1,6 @@
# garage # garage
![Version: 0.9.3](https://img.shields.io/badge/Version-0.9.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v2.3.0](https://img.shields.io/badge/AppVersion-v2.3.0-informational?style=flat-square) ![Version: 0.9.4](https://img.shields.io/badge/Version-0.9.4-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v2.4.0](https://img.shields.io/badge/AppVersion-v2.4.0-informational?style=flat-square)
S3-compatible object store for small self-hosted geo-distributed deployments S3-compatible object store for small self-hosted geo-distributed deployments
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_api_admin" name = "garage_api_admin"
version = "2.3.0" version = "2.4.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+2 -2
View File
@@ -191,7 +191,7 @@ impl RequestHandler for GetCurrentAdminTokenInfoRequest {
.admin .admin
.metrics_token .metrics_token
.as_ref() .as_ref()
.is_some_and(|s| s.eq_ct(&self.admin_token)) .is_some_and(|s| s == &self.admin_token)
{ {
return Ok(GetCurrentAdminTokenInfoResponse( return Ok(GetCurrentAdminTokenInfoResponse(
GetAdminTokenInfoResponse { GetAdminTokenInfoResponse {
@@ -210,7 +210,7 @@ impl RequestHandler for GetCurrentAdminTokenInfoRequest {
.admin .admin
.admin_token .admin_token
.as_ref() .as_ref()
.is_some_and(|s| s.eq_ct(&self.admin_token)) .is_some_and(|s| s == &self.admin_token)
{ {
return Ok(GetCurrentAdminTokenInfoResponse( return Ok(GetCurrentAdminTokenInfoResponse(
GetAdminTokenInfoResponse { GetAdminTokenInfoResponse {
+2 -8
View File
@@ -117,14 +117,8 @@ impl AdminApiServer {
#[cfg(feature = "metrics")] exporter: PrometheusExporter, #[cfg(feature = "metrics")] exporter: PrometheusExporter,
) -> Arc<Self> { ) -> Arc<Self> {
let cfg = &garage.config.admin; let cfg = &garage.config.admin;
let metrics_token = cfg let metrics_token = cfg.metrics_token.as_deref().map(hash_bearer_token);
.metrics_token let admin_token = cfg.admin_token.as_deref().map(hash_bearer_token);
.as_ref()
.map(|token| hash_bearer_token(token.extract_secret()));
let admin_token = cfg
.admin_token
.as_ref()
.map(|token| hash_bearer_token(token.extract_secret()));
let metrics_require_token = cfg.metrics_require_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());
+1 -1
View File
@@ -876,7 +876,7 @@ impl Modify for SecurityAddon {
#[derive(OpenApi)] #[derive(OpenApi)]
#[openapi( #[openapi(
info( info(
version = "v2.3.0", version = "v2.4.0",
title = "Garage administration API", title = "Garage administration API",
description = "Administrate your Garage cluster programmatically, including status, layout, keys, buckets, and maintenance tasks. description = "Administrate your Garage cluster programmatically, including status, layout, keys, buckets, and maintenance tasks.
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_api_common" name = "garage_api_common"
version = "2.3.0" version = "2.4.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_api_k2v" name = "garage_api_k2v"
version = "2.3.0" version = "2.4.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_api_s3" name = "garage_api_s3"
version = "2.3.0" version = "2.4.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+6 -1
View File
@@ -698,9 +698,14 @@ fn body_from_blocks_range(
// range, as well as their "true offset", which is their actual offset in the complete // range, as well as their "true offset", which is their actual offset in the complete
// file (whereas block.offset designates the offset of the block WITHIN THE PART // file (whereas block.offset designates the offset of the block WITHIN THE PART
// block.part_number, which is not the same in the case of a multipart upload) // block.part_number, which is not the same in the case of a multipart upload)
// A version with no blocks yields no data, so the capacity hint must not index
// into an empty slice.
let capacity_block_size = all_blocks
.first()
.map_or(1024, |(_, b)| std::cmp::max(b.size, 1024));
let mut blocks: Vec<(VersionBlock, u64)> = Vec::with_capacity(std::cmp::min( let mut blocks: Vec<(VersionBlock, u64)> = Vec::with_capacity(std::cmp::min(
all_blocks.len(), all_blocks.len(),
4 + ((end - begin) / std::cmp::max(all_blocks[0].1.size, 1024)) as usize, 4 + ((end - begin) / capacity_block_size) as usize,
)); ));
let mut block_offset: u64 = 0; let mut block_offset: u64 = 0;
for (_, b) in all_blocks.iter() { for (_, b) in all_blocks.iter() {
+7 -1
View File
@@ -476,7 +476,13 @@ pub async fn handle_complete_multipart_upload(
size: total_size, size: total_size,
etag: etag.clone(), etag: etag.clone(),
}, },
final_version.blocks.items()[0].1.hash, final_version
.blocks
.items()
.first()
.ok_or_internal_error("Multipart completion produced a final version with no blocks")?
.1
.hash,
)); ));
let final_object = Object::new(*bucket_id, key.clone(), vec![object_version]); let final_object = Object::new(*bucket_id, key.clone(), vec![object_version]);
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_block" name = "garage_block"
version = "2.3.0" version = "2.4.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_db" name = "garage_db"
version = "2.3.0" version = "2.4.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage" name = "garage"
version = "2.3.0" version = "2.4.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -2
View File
@@ -307,8 +307,7 @@ async fn cli_command(opt: Opt) -> Result<(), Error> {
let net_key_hex_str = rpc_secret.ok_or("No RPC secret provided")?; let net_key_hex_str = rpc_secret.ok_or("No RPC secret provided")?;
let network_key = NetworkKey::from_slice( let network_key = NetworkKey::from_slice(
&hex::decode(net_key_hex_str.extract_secret()) &hex::decode(&net_key_hex_str).err_context("Invalid RPC secret key (bad hex)")?[..],
.err_context("Invalid RPC secret key (bad hex)")?[..],
) )
.ok_or("Invalid RPC secret provided (wrong length)")?; .ok_or("Invalid RPC secret provided (wrong length)")?;
+5 -8
View File
@@ -2,7 +2,7 @@ use std::path::PathBuf;
use structopt::StructOpt; use structopt::StructOpt;
use garage_util::config::{Config, Secret}; use garage_util::config::Config;
use garage_util::error::Error; use garage_util::error::Error;
/// Structure for secret values or paths that are passed as CLI arguments or environment /// Structure for secret values or paths that are passed as CLI arguments or environment
@@ -99,7 +99,7 @@ pub fn fill_secrets(mut config: Config, secrets: Secrets) -> Result<Config, Erro
} }
pub(crate) fn fill_secret( pub(crate) fn fill_secret(
config_secret: &mut Option<Secret<String>>, config_secret: &mut Option<String>,
config_secret_file: &Option<PathBuf>, config_secret_file: &Option<PathBuf>,
cli_secret: &Option<String>, cli_secret: &Option<String>,
cli_secret_file: &Option<PathBuf>, cli_secret_file: &Option<PathBuf>,
@@ -110,7 +110,7 @@ pub(crate) fn fill_secret(
(Some(_), Some(_)) => { (Some(_), Some(_)) => {
return Err(format!("only one of `{}` and `{}_file` can be set", name, name).into()); return Err(format!("only one of `{}` and `{}_file` can be set", name, name).into());
} }
(Some(secret), None) => Some(Secret::new(secret.to_string())), (Some(secret), None) => Some(secret.to_string()),
(None, Some(file)) => Some(read_secret_file(file, allow_world_readable)?), (None, Some(file)) => Some(read_secret_file(file, allow_world_readable)?),
(None, None) => None, (None, None) => None,
}; };
@@ -132,10 +132,7 @@ pub(crate) fn fill_secret(
Ok(()) Ok(())
} }
fn read_secret_file( fn read_secret_file(file_path: &PathBuf, allow_world_readable: bool) -> Result<String, Error> {
file_path: &PathBuf,
allow_world_readable: bool,
) -> Result<Secret<String>, Error> {
if !allow_world_readable { if !allow_world_readable {
#[cfg(unix)] #[cfg(unix)]
{ {
@@ -155,7 +152,7 @@ fn read_secret_file(
// trim_end: allows for use case such as `echo "$(openssl rand -hex 32)" > somefile`. // trim_end: allows for use case such as `echo "$(openssl rand -hex 32)" > somefile`.
// also editors sometimes add a trailing newline // also editors sometimes add a trailing newline
Ok(Secret::new(String::from(secret_buf.trim_end()))) Ok(String::from(secret_buf.trim_end()))
} }
#[cfg(test)] #[cfg(test)]
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_model" name = "garage_model"
version = "2.3.0" version = "2.4.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -1
View File
@@ -137,7 +137,7 @@ impl Garage {
info!("Initializing RPC..."); info!("Initializing RPC...");
let network_key = hex::decode(config.rpc_secret.as_ref().ok_or_message( let network_key = hex::decode(config.rpc_secret.as_ref().ok_or_message(
"rpc_secret value is missing, not present in config file or in environment", "rpc_secret value is missing, not present in config file or in environment",
)?.extract_secret()) )?)
.ok() .ok()
.and_then(|x| NetworkKey::from_slice(&x)) .and_then(|x| NetworkKey::from_slice(&x))
.ok_or_message("Invalid RPC secret key: expected 32 bytes of random hex, please check the documentation for requirements")?; .ok_or_message("Invalid RPC secret key: expected 32 bytes of random hex, please check the documentation for requirements")?;
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_net" name = "garage_net"
version = "2.3.0" version = "2.4.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_rpc" name = "garage_rpc"
version = "2.3.0" version = "2.4.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -1
View File
@@ -115,7 +115,7 @@ impl ConsulDiscovery {
let mut headers = reqwest::header::HeaderMap::new(); let mut headers = reqwest::header::HeaderMap::new();
headers.insert( headers.insert(
"x-consul-token", "x-consul-token",
reqwest::header::HeaderValue::from_str(token.extract_secret())?, reqwest::header::HeaderValue::from_str(token)?,
); );
builder = builder.default_headers(headers); builder = builder.default_headers(headers);
} }
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_table" name = "garage_table"
version = "2.3.0" version = "2.4.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
+1 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_util" name = "garage_util"
version = "2.3.0" version = "2.4.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@@ -32,7 +32,6 @@ lazy_static.workspace = true
tracing.workspace = true tracing.workspace = true
rand.workspace = true rand.workspace = true
sha2.workspace = true sha2.workspace = true
subtle.workspace = true
chrono.workspace = true chrono.workspace = true
rmp-serde.workspace = true rmp-serde.workspace = true
+4 -35
View File
@@ -90,7 +90,7 @@ pub struct Config {
pub allow_world_readable_secrets: bool, pub allow_world_readable_secrets: bool,
/// RPC secret key: 32 bytes hex encoded /// RPC secret key: 32 bytes hex encoded
pub rpc_secret: Option<Secret<String>>, pub rpc_secret: Option<String>,
/// Optional file where RPC secret key is read from /// Optional file where RPC secret key is read from
pub rpc_secret_file: Option<PathBuf>, pub rpc_secret_file: Option<PathBuf>,
/// Address to bind for RPC /// Address to bind for RPC
@@ -205,37 +205,6 @@ pub struct WebConfig {
pub add_host_to_metrics: bool, pub add_host_to_metrics: bool,
} }
#[derive(Deserialize, Clone)]
#[serde(transparent)]
pub struct Secret<T>(T);
impl<T> Secret<T> {
pub fn new(secret: T) -> Self {
Secret(secret)
}
pub fn extract_secret(&self) -> &T {
&self.0
}
}
impl<T: std::ops::Deref<Target = str>> Secret<T> {
pub fn eq_ct(&self, other: &T) -> bool {
use subtle::ConstantTimeEq;
self.0
.deref()
.as_bytes()
.ct_eq(other.deref().as_bytes())
.into()
}
}
impl<T> std::fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Secret").finish_non_exhaustive()
}
}
/// Configuration for the admin and monitoring HTTP API /// Configuration for the admin and monitoring HTTP API
#[derive(Deserialize, Debug, Clone, Default)] #[derive(Deserialize, Debug, Clone, Default)]
pub struct AdminConfig { pub struct AdminConfig {
@@ -243,7 +212,7 @@ pub struct AdminConfig {
pub api_bind_addr: Option<UnixOrTCPSocketAddress>, pub api_bind_addr: Option<UnixOrTCPSocketAddress>,
/// Bearer token to use to scrape metrics /// Bearer token to use to scrape metrics
pub metrics_token: Option<Secret<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 /// Whether to require an access token for accessing the metrics endpoint
@@ -251,7 +220,7 @@ pub struct AdminConfig {
pub metrics_require_token: bool, 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<Secret<String>>, pub admin_token: Option<String>,
/// File to read admin token from /// File to read admin token from
pub admin_token_file: Option<PathBuf>, pub admin_token_file: Option<PathBuf>,
@@ -283,7 +252,7 @@ pub struct ConsulDiscoveryConfig {
/// Client TLS key to use when connecting to Consul /// Client TLS key to use when connecting to Consul
pub client_key: Option<String>, pub client_key: Option<String>,
/// /// Token to use for connecting to consul /// /// Token to use for connecting to consul
pub token: Option<Secret<String>>, pub token: Option<String>,
/// Skip TLS hostname verification /// Skip TLS hostname verification
#[serde(default)] #[serde(default)]
pub tls_skip_verify: bool, pub tls_skip_verify: bool,
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "garage_web" name = "garage_web"
version = "2.3.0" version = "2.4.0"
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"] authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"