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
87 changes: 50 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Then, add the flake to your `configuration.nix`:
# Use accessKeyFile and secretKeyFile instead:
accessKeyFile = "/run/secrets/rustfs-access-key"; # or use sops-nix, agenix, etc.
secretKeyFile = "/run/secrets/rustfs-secret-key";
volumes = "/var/lib/rustfs"; # Use a persistent location
pools = [ { volumes = [ "/var/lib/rustfs" ]; } ]; # Use a persistent location
address = ":9000";
consoleEnable = true;
consoleAddress = ":9001";
Expand Down Expand Up @@ -94,7 +94,7 @@ Then, add the flake to your `configuration.nix`:
package = inputs.rustfs.packages.${pkgs.stdenv.hostPlatform.system}.default;
accessKeyFile = config.sops.secrets.rustfs-access-key.path;
secretKeyFile = config.sops.secrets.rustfs-secret-key.path;
volumes = "/var/lib/rustfs";
pools = [ { volumes = [ "/var/lib/rustfs" ]; } ];
address = ":9000";
consoleEnable = true;
};
Expand Down Expand Up @@ -168,68 +168,81 @@ User account under which RustFS runs. The service runs as a dedicated non-root u

Group under which RustFS runs.

### services.rustfs.volumes
### services.rustfs.pools

**Type:** `string` or `list of strings`
**Type:** `list of submodules ({ nodes, volumes })`

**Default:** `["/var/lib/rustfs"]`
**Default:** `[ { volumes = [ "/var/lib/rustfs" ]; } ]`

List of paths or comma-separated string where RustFS stores data. Use persistent locations, not /tmp. Each entry must be
its own filesystem; several entries on one disk give no redundancy. Erasure coding needs at least 4 drives. Ignored when
`distributed.enable` is set — use `distributed.volumes` instead.
**Example:**

### services.rustfs.distributed.enable
```nix
services.rustfs.pools = [
{ nodes = [ "node1" "node2" "node3" "node4" ]; volumes = [ "/mnt/disk0" "/mnt/disk1" "/mnt/disk2" "/mnt/disk3" ]; }
{ nodes = [ "node5" "node6" "node7" "node8" ]; volumes = [ "/mnt/disk0" "/mnt/disk1" "/mnt/disk2" "/mnt/disk3" ]; }
];
```

**Type:** `bool`
Server pools, in order. Every RustFS deployment is a pool no matter if it is a single drive, one node of four, four nodes of four,
so this is the only place drives are declared. A pool with an empty `nodes` uses local paths; give it hostnames and its
drives become URL endpoints, which is what makes a deployment distributed.

Each entry takes `nodes` (hostnames making up the pool, resolvable from every node of it) and `volumes` (drive paths,
each on its own filesystem, with every node of the pool using the same layout). Use persistent locations, not /tmp:
several entries on one disk give no redundancy.

**Default:** `false`
Appending a pool is how a cluster grows without rebalancing what it already stores, and the only route from a
single-node deployment to a distributed one. Draining the old pool afterwards is `rc admin decommission`.

Whether to run as part of a distributed RustFS cluster spanning several nodes. When enabled, the module renders the
shared endpoint list (`http://<node>:<port><volume>` for every node × volume pair) that all nodes must agree on, ordered
drive-major so an erasure set spans nodes instead of sitting on one.
A lone pool is listed drive by drive, so its names take any shape. Several cannot be: RustFS reads plain arguments as
one pool and rejects mixing the two forms, so each pool has to collapse into a single ellipsis expression such as
`node{2...5}`. That needs a common prefix and a contiguous numeric range, and padding that is all or nothing `disk01`
alongside `disk2` would expand to a drive you never declared, so the module refuses it.

Keep the list identical and in the same order on every node: RustFS derives pool identity from it, so a divergent list
is a different cluster.

See [examples/distributed-cluster.nix](./examples/distributed-cluster.nix) for a complete four-node configuration.

### services.rustfs.distributed.nodes
### services.rustfs.port

**Type:** `port`

**Type:** `list of strings`
**Default:** `9000`

**Default:** `[]`
Port peers reach each other on. Must match the port in `address`, and be open between nodes in the firewall.

**Example:** `["node1" "node2" "node3" "node4"]`
### services.rustfs.erasureSetDriveCount

Hostnames of every node in the cluster, resolvable from each of them. Set identically on all nodes — the endpoint list is
rendered from this and must come out byte-identical cluster-wide. At least 4 nodes are required.
**Type:** `null or positive integer`

### services.rustfs.distributed.volumes
**Default:** `null`

**Type:** `list of strings`
**Example:** `4`

**Default:** `[]`
Drives per erasure set. Left null RustFS picks a divisor of the pool's drive count itself; set it when the split matters,
such as one set spanning all four nodes of a pool rather than sitting inside one. A pool's drive count must divide by it.

**Example:** `["/mnt/disk0" "/mnt/disk1" "/mnt/disk2" "/mnt/disk3"]`
### services.rustfs.storageClassStandardParity

Drive paths present on each node, each on its own filesystem. Every node uses the same layout, so this replaces
`volumes` in distributed mode and is what gets created and made writable locally. At least 4 drives per node are
required.
**Type:** `null or positive integer`

### services.rustfs.distributed.port
**Default:** `null`

**Type:** `port`
**Example:** `2`

**Default:** `9000`
Parity drives per erasure set for the STANDARD storage class. Two of four tolerates one node of a four-node set going
away while writes continue. Must be below `erasureSetDriveCount`.

Port peers reach each other on. Must match the port in `address`, and be open between nodes in the firewall.
### services.rustfs.storageClassRrsParity

### services.rustfs.distributed.localEndpointHost
**Type:** `null or positive integer`

**Type:** `string`
**Default:** `null`

**Default:** `config.networking.hostName`
**Example:** `1`

Which entry of `nodes` identifies this machine, so it claims its own drives instead of reaching them over RPC. Required
whenever `address` binds a wildcard such as `0.0.0.0`, since RustFS cannot infer its identity from that and would
otherwise treat every drive as remote.
Parity drives per erasure set for the REDUCED_REDUNDANCY class. Must be below `erasureSetDriveCount`.

> **Note**: All nodes must share the *same* access/secret key pair, and it must not be the default
> `rustfsadmin`/`rustfsadmin` — RustFS derives the inter-node RPC secret from the credentials and refuses to derive one
Expand Down
16 changes: 8 additions & 8 deletions docs/IMPROVEMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,11 @@ tlsDirectory = "/etc/rustfs/tls";
```nix
services.rustfs = {
enable = true;
accessKey = "rustfsadmin"; # ❌ In Nix store (world-readable)
secretKey = "rustfsadmin"; # ❌ In Nix store (world-readable)
volumes = "/tmp/rustfs"; # ❌ Temporary storage
# Running as root ❌ Excessive privileges
# No systemd hardening ❌ No sandboxing
accessKey = "rustfsadmin"; # ❌ In Nix store (world-readable)
secretKey = "rustfsadmin"; # ❌ In Nix store (world-readable)
pools = [ { volumes = [ "/tmp/rustfs" ]; } ]; # ❌ Temporary storage
# Running as root ❌ Excessive privileges
# No systemd hardening ❌ No sandboxing
};
```

Expand All @@ -367,9 +367,9 @@ services.rustfs = {
enable = true;
accessKeyFile = config.sops.secrets.rustfs-access-key.path; # ✅ Encrypted
secretKeyFile = config.sops.secrets.rustfs-secret-key.path; # ✅ Encrypted
volumes = "/var/lib/rustfs"; # ✅ Persistent
# Runs as unprivileged user ✅ Least privilege
# Comprehensive systemd hardening ✅ Defense in depth
pools = [ { volumes = [ "/var/lib/rustfs" ]; } ]; # ✅ Persistent
# Runs as unprivileged user ✅ Least privilege
# Comprehensive systemd hardening ✅ Defense in depth
consoleAddress = "127.0.0.1:9001"; # ✅ Localhost only
};
```
Expand Down
8 changes: 4 additions & 4 deletions docs/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ services.rustfs = {
enable = true;
accessKey = "rustfsadmin"; # ❌ INSECURE!
secretKey = "rustfsadmin"; # ❌ INSECURE!
volumes = "/tmp/rustfs";
pools = [ { volumes = [ "/tmp/rustfs" ]; } ];
address = ":9000";
};
```
Expand Down Expand Up @@ -159,7 +159,7 @@ services.rustfs = {
enable = true;
accessKeyFile = config.sops.secrets.rustfs-access-key.path; # ✅ SECURE
secretKeyFile = config.sops.secrets.rustfs-secret-key.path; # ✅ SECURE
volumes = "/var/lib/rustfs"; # Use persistent storage
pools = [ { volumes = [ "/var/lib/rustfs" ]; } ]; # Use persistent storage
address = ":9000";
consoleAddress = "127.0.0.1:9001"; # Localhost only
};
Expand Down Expand Up @@ -194,7 +194,7 @@ services.rustfs = {
enable = true;
accessKeyFile = config.age.secrets.rustfs-access-key.path;
secretKeyFile = config.age.secrets.rustfs-secret-key.path;
volumes = "/var/lib/rustfs";
pools = [ { volumes = [ "/var/lib/rustfs" ]; } ];
address = ":9000";
};
}
Expand All @@ -207,7 +207,7 @@ services.rustfs = {
enable = true;
accessKeyFile = "/run/secrets/rustfs-access-key";
secretKeyFile = "/run/secrets/rustfs-secret-key";
volumes = "/var/lib/rustfs";
pools = [ { volumes = [ "/var/lib/rustfs" ]; } ];
address = ":9000";
};
```
Expand Down
4 changes: 2 additions & 2 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,10 @@ Use appropriate volume locations:
```nix
services.rustfs = {
# ❌ Bad - temporary storage
volumes = "/tmp/rustfs";
pools = [ { volumes = [ "/tmp/rustfs" ]; } ];

# ✅ Good - persistent storage with proper permissions
volumes = "/var/lib/rustfs";
pools = [ { volumes = [ "/var/lib/rustfs" ]; } ];

# ✅ Also good - multiple volumes
volumes = [ "/mnt/storage1" "/mnt/storage2" ];
Expand Down
37 changes: 16 additions & 21 deletions examples/distributed-cluster.nix
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,15 @@
# RustFS distributed cluster NixOS Configuration Example
#
# Four nodes with four drives each. This exact file is imported by every node —
# `services.rustfs.distributed` is deliberately identical everywhere, and the
# `services.rustfs.pools` is deliberately identical everywhere, and the
# module derives each node's identity from `networking.hostName`.
#
# Requirements the module asserts at evaluation time:
# - at least 4 nodes in `distributed.nodes`
# - at least 4 drives in `distributed.volumes`
# - `distributed.localEndpointHost` is one of `distributed.nodes`
# - at least 4 nodes and 4 drives in the pool
#
# Additional requirements the module cannot check for you:
# - every hostname in `nodes` resolves from every other node (DNS or
# `networking.hosts`), and `distributed.port` is reachable between them
# `networking.hosts`), and `port` is reachable between them
# - the access/secret key pair is identical on all nodes, and is *not* the
# default rustfsadmin/rustfsadmin — RustFS derives the inter-node RPC secret
# from the credentials and refuses to derive one from the defaults
Expand Down Expand Up @@ -68,25 +66,22 @@ in
services.rustfs = {
enable = true;

distributed = {
enable = true;
# One pool spanning the fleet. The module expands it into the shared
# endpoint list (http://<node>:<port><volume>) every node must agree on.
# Appending a second pool here is how the cluster grows later; RustFS works
# out which drives are this machine's by resolving the endpoint hosts.
pools = [{ inherit nodes volumes; }];

# Replaces `volumes` when distributed mode is on: these are the local
# drives, and the module expands them into the shared endpoint list
# (http://<node>:<port><volume>) that every node must agree on.
inherit nodes volumes;
port = 9000;

port = 9000;
# Sets of four across sixteen drives, two of them parity. Because the module
# lays the endpoints out drive-major, a set spans the four nodes rather than
# sitting on one, so a whole node can go away and writes still have quorum.
erasureSetDriveCount = 4;
storageClassStandardParity = 2;
storageClassRrsParity = 1;

# Which entry of `nodes` is this machine. The default is
# config.networking.hostName, which is correct as long as the hostname
# matches the name used in `nodes`. Set it explicitly when it does not —
# required whenever `address` binds a wildcard, since RustFS cannot infer
# its own identity from 0.0.0.0 and would treat every drive as remote.
localEndpointHost = config.networking.hostName;
};

# Must bind an address peers can reach, on `distributed.port`.
# Must bind an address peers can reach, on `port`.
address = "0.0.0.0:9000";

# SECURITY: Bind console to localhost only, access via SSH tunnel
Expand Down
2 changes: 1 addition & 1 deletion examples/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
enable = true;
package = rustfs-flake.packages.${pkgs.stdenv.hostPlatform.system}.default;

volumes = "/var/lib/rustfs/data";
pools = [{ volumes = [ "/var/lib/rustfs/data" ]; }];
address = "0.0.0.0:9000";
consoleEnable = true;
consoleAddress = "0.0.0.0:9001";
Expand Down
2 changes: 1 addition & 1 deletion examples/nixos-configuration.nix
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
enable = true;

# Storage path - use persistent storage, not /tmp
volumes = "/var/lib/rustfs/data";
pools = [{ volumes = [ "/var/lib/rustfs/data" ]; }];

# API server address (Port 9000)
# Use "0.0.0.0:9000" or ":9000" to listen on all interfaces
Expand Down
25 changes: 18 additions & 7 deletions examples/single-node-multi-disk.nix
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,26 @@
services.rustfs = {
enable = true;

# One entry per drive. A comma-separated string works too, but the list form
# is easier to read and to generate.
volumes = [
"/mnt/rustfs0"
"/mnt/rustfs1"
"/mnt/rustfs2"
"/mnt/rustfs3"
# One pool, one entry per drive. Giving the pool `nodes` as well is what
# turns this into a distributed deployment.
pools = [
{
volumes = [
"/mnt/rustfs0"
"/mnt/rustfs1"
"/mnt/rustfs2"
"/mnt/rustfs3"
];
}
];

# One erasure set across all four drives, two of them parity: any one drive
# may fail while reads and writes continue. Left unset RustFS picks a split
# itself, which works but leaves the redundancy implicit.
erasureSetDriveCount = 4;
storageClassStandardParity = 2;
storageClassRrsParity = 1;

address = ":9000";

# SECURITY: Bind console to localhost only, access via SSH tunnel
Expand Down
Loading