Skip to content

fix(rest,satellite): encryption create-passphrase unlocks LUKS provisioning#143

Merged
Andrei Kvapil (kvaps) merged 4 commits into
mainfrom
fix/encryption-passphrase-unlocks-luks
Jun 12, 2026
Merged

fix(rest,satellite): encryption create-passphrase unlocks LUKS provisioning#143
Andrei Kvapil (kvaps) merged 4 commits into
mainfrom
fix/encryption-passphrase-unlocks-luks

Conversation

@kvaps

@kvaps Andrei Kvapil (kvaps) commented Jun 12, 2026

Copy link
Copy Markdown
Member

Problem

linstor encryption create-passphrase stores the cluster master passphrase in a Secret, but nothing downstream ever read that Secret:

  • the LUKS RD-create gate only consulted the DrbdOptions/EncryptPassphrase controller property, so the upstream-standard flow (encryption create-passphraserd create --layer-list drbd,luks,storage) failed with 400 — and the error hint instructed operators to store a PLAINTEXT passphrase in a controller prop;
  • the satellite-facing dispatch lifts the LUKS key onto the LuksPassphrase wire prop only from controller-scope props, so even with the gate fixed, a Secret-only cluster would loop on LUKS in layer stack but Props.LuksPassphrase empty at apply time.

What changed

  • New pkg/passphrase package centralises the Secret resolution (ControllerConfig PassphraseSecretRef → default blockstor-cluster-passphrase); the REST encryption handlers now write/read through it.
  • The LUKS RD-create gate accepts the master passphrase from the encryption Secret as the primary, upstream-parity mechanism. The legacy controller-prop path keeps working for existing clusters (marked deprecated in code comments); the refusal hint now leads with encryption create-passphrase.
  • The satellite's ResourceReconciler folds the Secret value into the resolved effective props under the canonical DrbdOptions/EncryptPassphrase key before BuildDesired, so the passphrase travels the exact channel the legacy prop used — lifted onto the LuksPassphrase wire prop and stripped from the rendered DRBD options. Operator-set props keep precedence over the Secret so existing LUKS volumes keep unlocking with the key they were formatted with.
  • The Secret is read with the uncached API reader (avoids spinning up a cluster-wide Secret informer) in the controller namespace; the satellite gains a --controller-namespace flag defaulting to $POD_NAMESPACE then blockstor-system, and the stand DaemonSet now passes POD_NAMESPACE via the downward API. Satellite RBAC already grants read access to Secrets.

Testing

  • L1: pkg/rest/luks_gate_bug023_test.go — create-passphrase → LUKS RD create succeeds (was 400), GitOps-seeded Secret accepted, legacy prop path still accepted, refusal hints at create-passphrase; pkg/satellite/controllers/luks_passphrase_internal_test.go — Secret value reaches the LuksPassphrase wire prop via BuildDesired, legacy-prop precedence preserved, custom PassphraseSecretRef honoured, safe no-ops for non-LUKS stacks and missing Secrets.
  • Tier 2: TestGroupKWFLUKSStackEndToEnd (operator CLI flow over envtest) goes red → green with this change.
  • go test ./... green, golangci-lint run 0 issues.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added --controller-namespace CLI flag to the satellite process for specifying where cluster master passphrase secrets are located.
    • LUKS volumes now automatically integrate with the cluster master encryption passphrase secret.
    • Users can provision passphrases via REST API or Kubernetes secrets.
  • Documentation

    • Added clarification on --controller-namespace flag behavior and defaults.
  • Tests

    • Added integration and unit tests for passphrase handling and LUKS encryption workflows.

Andrei Kvapil (kvaps) and others added 3 commits June 12, 2026 05:07
The LUKS RD-create gate (Bug 95) only consulted the legacy
DrbdOptions/EncryptPassphrase controller prop, so the
upstream-standard flow - linstor encryption create-passphrase
followed by creating a LUKS-layered RD - failed with 400, and the
error hint told operators to store a plaintext passphrase in a
controller prop.

Hoist the Secret access into a shared pkg/passphrase package and
make the gate consult the encryption Secret first (primary,
upstream-parity), keeping the legacy prop working for backward
compatibility. The refusal hint now leads with
encryption create-passphrase and marks the prop path deprecated.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
…ayer

The satellite lifts the LUKS key onto the LuksPassphrase wire prop
only from the controller-scope props, so a cluster whose passphrase
lives solely in the encryption Secret passed the RD-create gate but
looped on 'Props.LuksPassphrase empty' at apply time.

Fold the Secret value into the resolved effective props under the
canonical DrbdOptions/EncryptPassphrase key before BuildDesired, so
the passphrase travels the exact channel the legacy prop used
(lifted to LuksPassphrase, stripped from rendered DRBD options).
Operator-set props keep precedence so existing volumes keep their
key. The Secret is read through the uncached APIReader in the
controller namespace (new --controller-namespace flag, default
$POD_NAMESPACE then blockstor-system); satellite RBAC already
grants secrets read.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
L1 regression suite for the encryption-Secret flow: REST gate
accepts LUKS RD creation after create-passphrase (or a directly
seeded Secret), keeps the legacy controller-prop path working, and
hints at create-passphrase on refusal; satellite-side injection
delivers the Secret value to the LuksPassphrase wire prop with
legacy-prop precedence, custom PassphraseSecretRef resolution, and
safe no-ops for non-LUKS stacks and missing Secrets.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR centralizes the cluster master encryption passphrase mechanism for LUKS volume support. It introduces a shared pkg/passphrase package to resolve and read the passphrase Secret, adds namespace-aware resolution in the satellite process via a new --controller-namespace CLI flag, updates the REST LUKS gate logic to validate passphrases from Secrets, and implements controller-side injection to supply the passphrase to satellite-reconciled LUKS resources.

Changes

Bug 023: Cluster Master Encryption Passphrase Centralization

Layer / File(s) Summary
Shared passphrase package and infrastructure
pkg/passphrase/passphrase.go, .golangci.yml
New SecretName(ctx, reader) and Read(ctx, reader, namespace) functions resolve and read the cluster master encryption passphrase Secret, with CRD-based fallback and non-error handling for missing Secrets. Linter rule added to exclude godox from pkg/passphrase/.
Satellite namespace resolution and wiring
cmd/satellite/main.go, pkg/satellite/controllers/config.go, stand/blockstor-satellite-daemonset.yaml, pkg/satellite/stream/redial_spec_test.go
New --controller-namespace flag resolves via precedence (explicit flag → $POD_NAMESPACEblockstor-system); namespace is threaded into controllers.Config.Namespace to enable Secret lookups. DaemonSet now supplies POD_NAMESPACE via downward API.
REST encryption endpoint refactoring
pkg/rest/encryption.go
Passphrase read/write logic delegates to passphrase.SecretName() and passphrase.Read(), removing local ControllerConfig-based resolution.
LUKS gate logic in REST resource-definitions
pkg/rest/resource_definitions.go
LUKS creation prerequisite check now validates passphrase via the encryption Secret first, then falls back to legacy controller props. Error message and remediation hint updated to guide operators to linstor encryption create-passphrase.
REST integration tests for LUKS gate
pkg/rest/luks_gate_bug023_test.go
Four Bug 023 tests verify: (1) passphrase endpoint creation unlocks LUKS RD creation; (2) direct Secret seeding unlocks LUKS RD; (3) legacy controller-prop still works (backward compatibility); (4) missing passphrase returns 400 with remediation hint and refuses RD.
Controller-side passphrase injection
pkg/satellite/controllers/luks_passphrase.go, pkg/satellite/controllers/resource.go
New injectLUKSMasterPassphrase helper reads the Secret-derived passphrase and injects it into effective props for LUKS resources. Wired into buildDesiredFromCRD before dispatcher handoff.
Controller-side unit tests for passphrase injection
pkg/satellite/controllers/luks_passphrase_internal_test.go
Five Bug 023 tests verify: Secret passphrase injection reaches wire props; legacy controller props retain precedence; non-LUKS resources skip injection; missing Secrets tolerate gracefully; custom ControllerConfig.PassphraseSecretRef is honored.
Documentation update
tests/scenarios/03-networking.md
Clarifying note explains --controller-namespace flag selects the Kubernetes namespace for passphrase Secret lookup with documented fallback chain.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • cozystack/blockstor#110: Updates operator replay flow to set controller set-property DrbdOptions/EncryptPassphrase before LUKS RD creation, directly aligning with the changed LUKS passphrase guard logic and canonical key handling.

Poem

🐰 A passphrase finds its secret home,
In namespace Kubernetes will roam,
LUKS gates open, REST and spec align,
The master key through layers does shine!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly addresses the main objective: enabling the REST command 'encryption create-passphrase' to unlock LUKS provisioning by reading the Secret instead of relying solely on the legacy controller property.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/encryption-passphrase-unlocks-luks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request centralizes access to the cluster master passphrase Secret under a new pkg/passphrase package to resolve Bug 023. Previously, the LUKS resource definition creation gate and satellite-side dispatch only checked the legacy plaintext controller property DrbdOptions/EncryptPassphrase, ignoring the Secret written by linstor encryption create-passphrase. The changes integrate the new package into the REST and satellite controllers, introduce a --controller-namespace flag to the satellite, and add comprehensive unit tests. The review feedback suggests optimizing the Secret retrieval in the satellite's injectLUKSMasterPassphrase function to use the cached client for resolving the Secret name, thereby reducing API server load while still avoiding informer stalls by using the uncached reader only for the Secret itself.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +21 to +30
import (
"context"
"strings"

"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"

blockstoriov1alpha1 "github.com/cozystack/blockstor/api/v1alpha1"
"github.com/cozystack/blockstor/pkg/passphrase"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add imports for corev1 and apierrors to support the optimized Secret retrieval in injectLUKSMasterPassphrase.

Suggested change
import (
"context"
"strings"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
blockstoriov1alpha1 "github.com/cozystack/blockstor/api/v1alpha1"
"github.com/cozystack/blockstor/pkg/passphrase"
)
import (
"context"
"strings"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
blockstoriov1alpha1 "github.com/cozystack/blockstor/api/v1alpha1"
"github.com/cozystack/blockstor/pkg/passphrase"
)

Comment on lines +81 to +92
pass, err := passphrase.Read(ctx, r.secretReader(), r.Config.Namespace)
if err != nil {
log.FromContext(ctx).Error(err,
"read cluster master passphrase Secret; LUKS apply will surface the missing key",
"namespace", r.Config.Namespace)

return
}

if pass == "" {
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To avoid making direct, uncached API server calls for the ControllerConfig singleton on every periodic reconciliation loop (which runs every 10 seconds for active DRBD resources), we can use the cached client (r.Client) to resolve the Secret name. We only use the uncached APIReader (r.secretReader()) to read the Secret itself, which preserves the fix for the Bug 110 informer stall while significantly reducing API server load.

	secretName, err := passphrase.SecretName(ctx, r.Client)
	if err != nil {
		log.FromContext(ctx).Error(err,
			"resolve passphrase Secret name; LUKS apply will surface the missing key")
		return
	}

	var sec corev1.Secret
	if err := r.secretReader().Get(ctx, client.ObjectKey{Namespace: r.Config.Namespace, Name: secretName}, &sec); err != nil {
		if !apierrors.IsNotFound(err) {
			log.FromContext(ctx).Error(err,
				"read cluster master passphrase Secret; LUKS apply will surface the missing key",
				"namespace", r.Config.Namespace, "name", secretName)
		}
		return
	}

	pass := string(sec.Data[passphrase.SecretKey])
	if pass == "" {
		return
	}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
pkg/rest/luks_gate_bug023_test.go (1)

120-127: 💤 Low value

Consider asserting LayerStack length or order for stronger validation.

The test verifies LUKS is in the stack via LayerInStack, but doesn't assert the full expected stack ["DRBD", "LUKS", "STORAGE"]. If a bug caused layer reordering or dropping other layers, this test would still pass. Consider strengthening the assertion:

if len(rd.LayerStack) != 3 || rd.LayerStack[0] != "DRBD" || rd.LayerStack[1] != "LUKS" || rd.LayerStack[2] != "STORAGE" {
    t.Errorf("LayerStack mismatch: got %v, want [DRBD LUKS STORAGE]", rd.LayerStack)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/rest/luks_gate_bug023_test.go` around lines 120 - 127, The test only
checks presence of LUKS via apiv1.LayerInStack but should assert the full
expected order and length to catch reorders/drops; update the test around rd :=
... ResourceDefinitions().Get(...) to additionally assert rd.LayerStack has
length 3 and that rd.LayerStack[0]=="DRBD", rd.LayerStack[1]=="LUKS", and
rd.LayerStack[2]=="STORAGE", emitting a clear t.Errorf message showing the
actual rd.LayerStack when the check fails.
cmd/satellite/main.go (1)

644-661: 💤 Low value

Consider extracting namespace resolution to a shared package.

The resolveControllerNamespace function duplicates logic from cmd/apiserver/main.go (context snippet lines 159-172). While the comment on line 649 acknowledges this is intentional to keep the binaries in sync, extracting the precedence logic to a shared package (e.g., pkg/config) would eliminate duplication and ensure the two implementations never drift without requiring manual synchronization.

If the duplication is deliberately maintained for build-time independence or deployment simplicity, this comment can be disregarded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/satellite/main.go` around lines 644 - 661, The resolveControllerNamespace
function duplicates namespace-precedence logic found in cmd/apiserver/main.go;
extract this logic into a shared package (e.g., pkg/config) as a single exported
function like ResolveControllerNamespace or ControllerNamespace that implements
the same precedence (flag -> POD_NAMESPACE -> "blockstor-system"), update
cmd/satellite/main.go to call the new pkg/config function instead of its local
resolveControllerNamespace, and update cmd/apiserver/main.go to use the same
shared function so both binaries share one source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cmd/satellite/main.go`:
- Around line 644-661: The resolveControllerNamespace function duplicates
namespace-precedence logic found in cmd/apiserver/main.go; extract this logic
into a shared package (e.g., pkg/config) as a single exported function like
ResolveControllerNamespace or ControllerNamespace that implements the same
precedence (flag -> POD_NAMESPACE -> "blockstor-system"), update
cmd/satellite/main.go to call the new pkg/config function instead of its local
resolveControllerNamespace, and update cmd/apiserver/main.go to use the same
shared function so both binaries share one source of truth.

In `@pkg/rest/luks_gate_bug023_test.go`:
- Around line 120-127: The test only checks presence of LUKS via
apiv1.LayerInStack but should assert the full expected order and length to catch
reorders/drops; update the test around rd := ... ResourceDefinitions().Get(...)
to additionally assert rd.LayerStack has length 3 and that
rd.LayerStack[0]=="DRBD", rd.LayerStack[1]=="LUKS", and
rd.LayerStack[2]=="STORAGE", emitting a clear t.Errorf message showing the
actual rd.LayerStack when the check fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 00f3c363-b124-47f6-a191-92ddb5edf0a5

📥 Commits

Reviewing files that changed from the base of the PR and between 960f414 and 8fe7fbc.

📒 Files selected for processing (13)
  • .golangci.yml
  • cmd/satellite/main.go
  • pkg/passphrase/passphrase.go
  • pkg/rest/encryption.go
  • pkg/rest/luks_gate_bug023_test.go
  • pkg/rest/resource_definitions.go
  • pkg/satellite/controllers/config.go
  • pkg/satellite/controllers/luks_passphrase.go
  • pkg/satellite/controllers/luks_passphrase_internal_test.go
  • pkg/satellite/controllers/resource.go
  • pkg/satellite/stream/redial_spec_test.go
  • stand/blockstor-satellite-daemonset.yaml
  • tests/scenarios/03-networking.md

@kvaps Andrei Kvapil (kvaps) merged commit 04aacdd into main Jun 12, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant