blockstor mirrors LINSTOR's layer_list model: a ResourceDefinition
declares an ordered chain of layers, each adding capabilities on top
of the layer below. The satellite walks the chain bottom-up when
provisioning and top-down when tearing down.
| Layer | What it does |
|---|---|
STORAGE |
Allocates the raw block device (LVM-thin LV, ZFS volume, loopfile, …) |
LUKS |
cryptsetup luksFormat + luksOpen over the storage device |
DRBD |
Renders .res and runs drbdadm to replicate over the network |
The first entry in layerStack is the topmost layer the consumer Pod
mounts. The last entry is always STORAGE (the storage layer is
required — every replica needs a backing device, even diskless ones
where it's the network leg).
| Stack | Use case |
|---|---|
["DRBD","STORAGE"] |
Default. Replicated PVC, the cozystack production case. |
["LUKS","STORAGE"] |
Single-replica encrypted PVC. No DRBD overhead. |
["DRBD","LUKS","STORAGE"] |
Encrypted at-rest + replicated. Per-volume cipher. |
["STORAGE"] |
Single-replica local mode. Ephemeral cache, scratch, ZFS dataset. |
The default is ["DRBD","STORAGE"] — when RD.Spec.LayerStack is
empty the controller inherits from the parent
ResourceGroup.Spec.SelectFilter.LayerStack, and when both are empty
the dispatcher falls through to the satellite's default-DRBD path.
curl -XPOST http://controller:3370/v1/resource-definitions \
-H 'Content-Type: application/json' \
-d '{"resource_definition":{"name":"pvc-1","layer_stack":["LUKS","STORAGE"]}}'linstor rg create encrypted-rg
linstor rg sp encrypted-rg DrbdOptions/Encryption/passphrase '<32-byte-secret>'
linstor rg c --place-count 2 encrypted-rg
linstor rg spawn encrypted-rg pvc-2 1G --layer-list LUKS DRBD(blockstor accepts linstor's --layer-list flag verbatim because
the REST shape mirrors upstream LINSTOR.)
apiVersion: blockstor.cozystack.io/v1alpha1
kind: ResourceDefinition
metadata: {name: pvc-encrypted}
spec:
layerStack: ["LUKS", "STORAGE"]
props:
DrbdOptions/Encryption/passphrase: "32-byte-secret"
volumeDefinitions:
- {volumeNumber: 0, sizeKib: 1048576}- Passphrase is per-RD via
DrbdOptions/Encryption/passphrase. The upstreamlinstor rd set-propertykey. - The dispatcher folds the resolver-resolved passphrase onto the wire
as
DesiredResource.Props["LuksPassphrase"]; the satellite reads it from there. - Empty passphrase with
LUKSin the stack fails the apply rather than silently producing an unencrypted volume. - Mapper name:
<rd>-<vol>-luks→/dev/mapper/<rd>-<vol>-luks. Stable across reconciles so reopen on satellite restart re-uses it. - Volume grow: the satellite runs
cryptsetup resizeon the mapper after the storage layer has resized the underlying LV, before DRBD resizes the replicated device.
- Pluggable layer ordering: the stack must be one of the four
rows in the table above. Arbitrary orderings (e.g.
["LUKS","DRBD","STORAGE"]with LUKS-over-DRBD instead of DRBD-over-LUKS) aren't yet validated or rendered. - Per-volume LUKS keys: every volume on a 2-volume RD currently uses the same RD-level passphrase. Per-volume keys (with master-key wrapping in the controller's KV store) is a follow-up.
- Mid-stack changes: editing
layerStackafter a Resource is active doesn't re-encrypt or unwrap existing data. Treat it as set-once-per-RD until migration support lands. - Cluster-passphrase rotation: the cluster passphrase
(
POST /v1/encryption/passphrase) wraps RD passphrases in the controller's KV store; rotating it doesn't re-encrypt LUKS headers. Rotating the per-RD passphrase requirescryptsetup luksChangeKeywhich isn't wired yet — operators should drop the RD and recreate.
pkg/api/v1/layer_stack_test.go— RD → RG → default resolution.pkg/satellite/reconciler_drbd_test.go::TestApplySkipsDRBDWhenLayerStackOmits—["STORAGE"]produces no.resand no drbdadm.pkg/satellite/reconciler_drbd_test.go::TestApplyLayersLUKS— pin cryptsetup luksFormat + luksOpen run on first activation.pkg/satellite/reconciler_drbd_test.go::TestApplyLUKSFailsWithoutPassphrase— pin the explicit-error path.