Skip to content

feat: add PKCS#11/HSM support for Fulcio and CTLog signing - #2128

Open
sampras343 wants to merge 2 commits into
mainfrom
sachin/feat/pkcs11-v2-ctlog
Open

feat: add PKCS#11/HSM support for Fulcio and CTLog signing#2128
sampras343 wants to merge 2 commits into
mainfrom
sachin/feat/pkcs11-v2-ctlog

Conversation

@sampras343

@sampras343 sampras343 commented Jul 23, 2026

Copy link
Copy Markdown
Member

Summary

Add PKCS#11/HSM support to the operator for both Fulcio (CA certificate signing) and CTLog (Signed Tree Head signing), enabling enterprises to use Hardware Security Modules for all signing keys via a vendor-agnostic plugin model. Integrates into the spec.signer hierarchy.

Fulcio PKCS#11 (spec.fulcio.signer)

  • type: pkcs11 on FulcioSigner with CEL mutual-exclusion rules
  • FulcioPKCS11Config: configRef (crypto11.conf) + keyConfig (HSM key ID/label)
  • signer.auth (Auth struct) for server container env vars — replaces custom CredentialsRef/ServerEnv
  • signer.certificateChain.certificateChainRef for pre-provisioned root CA from key ceremony
  • Top-level initContainers/volumes/volumeMounts on FulcioSpec (pod-wide, not nested under pkcs11)

CTLog PKCS#11 (spec.ctlog.signer)

  • type: pkcs11 on CTlogSigner with CEL mutual-exclusion rules
  • CTlogPKCS11Config: pinSecretRef, publicKeyRef, tokenLabel, modulePath, persistence
  • signer.auth for server container env vars (uniform with Fulcio)
  • Config-driven dispatch: generates keyspb.PKCS11Config protobuf instead of keyspb.PEMKeyFile
  • Status.PublicKeyRef populated from EnsurePKCS11Config for trust material resolution
  • Top-level initContainers/volumes/volumeMounts on CTlogSpec

Design decisions (per review feedback)

  • Reuse Auth struct instead of custom CredentialsRef/ServerEnv — matches TSA's pattern
  • Move initContainers/volumes/volumeMounts to top-level spec — pod-wide resources, not backend-specific
  • Drop PKCS11Status structs and drift detectionObservedGeneration suffices
  • Rename configRef/modulePath — drop redundant PKCS11 prefix (already scoped under pkcs11:)
  • Drop initContainers CEL rule — allow CSI drivers/webhook injectors to provision libraries without declaring init containers
  • Stop auto-injecting HSM_PIN — users set it explicitly in initContainers[].env if their vendor image needs it
  • Auth on Signer — uniform placement for both Fulcio and CTLog
  • Call Signer.SetDefaults() from SecureSign.SetDefaults()signer.type: file is explicit on the parent CR, CEL rules don't need !has(self.type) guards

Shared

  • PKCS11InitContainerSpec — curated corev1.Container subset shared by both components
  • v1alpha1 conversion: PKCS#11 fields preserved via MarshalData annotations (v1 API only)
  • Vendor-agnostic sample CR with <placeholder> values

E2E Validation (OCP 4.22)

Test Result
Fulcio PKCS#11 with SoftHSM (P-256) cosign sign + verify PASS
CTLog PKCS#11 with SoftHSM (P-256) cosign sign + verify PASS
Dual PKCS#11 (Fulcio + CTLog) cosign sign + verify PASS
File mode regression (no PKCS#11 fields) cosign sign + verify PASS
signer.type: file defaulted on SecureSign CR Verified
PKCS#11-built ct_server in file mode Zero impact, identical behavior

Dependency

SECURESIGN-5021: RHTAS ct_server image must be built with CGO_ENABLED=1 -tags=pkcs11,no_openssl. The go-toolset base image already includes gcc and glibc-devel — no dnf install needed (hermetic build compatible).

Test plan

  • make build and make test pass
  • Deploy file-mode CR — signer.type: file defaulted, no PKCS#11 conditions/args/init containers
  • Deploy dual PKCS#11 CR — full stack reaches Ready
  • cosign sign + verify passes in both file and PKCS#11 modes
  • v1alpha1 round-trip: PKCS#11 fields preserved through conversion
  • Mode switch: change signer.type from pkcs11 to file — PKCS#11 artifacts cleaned up

Jira: SECURESIGN-5014

@sampras343 sampras343 changed the title feat: add PKCS#11/HSM support for CTLog signing feat: add PKCS#11/HSM support Jul 23, 2026
@sampras343 sampras343 changed the title feat: add PKCS#11/HSM support feat: add PKCS#11/HSM support for Fulcio and CTLog signing Jul 23, 2026
@qodo-for-securesign

Copy link
Copy Markdown

PR Summary by Qodo

feat: PKCS#11/HSM support for CTLog signing (SECURESIGN-5021)

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Adds signerType: pkcs11 to CTlog CRD, enabling HSM-backed signing of Signed Tree Heads via
 keyspb.PKCS11Config.
• Introduces EnsurePKCS11Config action for CTLog to validate Secret refs, populate status, and
 detect drift/key rotation.
• Extends Deployment/ServerConfig actions to branch between file-based and PKCS#11 signing (init
 containers, HSM volumes, --pkcs11_module_path).
• Builds on Fulcio's PKCS#11 support (shared PKCS11InitContainerSpec, ensureVolumeDefaultMode)
 and refactors Fulcio deployment into file-CA vs PKCS#11-CA paths.
• Preserves new v1-only fields (SignerType, PKCS11, CAType) across v1alpha1 conversion via
 MarshalData annotations.
• Adds a vendor-agnostic sample CR for dual Fulcio+CTLog PKCS#11 deployment and new unit/roundtrip
 tests.
Diagram

graph TD
    CR["CTlog CR (signerType: pkcs11)"] --> Ensure["EnsurePKCS11Config"] --> Status[("Status.PKCS11 / PublicKeyRef")] 
    Status --> ServerCfg["ServerConfig action"] --> ProtoCfg["CreateCtlogPKCS11Config"] --> CM[("ConfigMap: keyspb.PKCS11Config")] 
    Status --> Deploy["Deployment action"] --> Init["HSM Init Containers"] --> Pod["CTLog Pod (--pkcs11_module_path)"] 
    CM --> Pod --> HSM{{"External HSM / SoftHSM"}}

    subgraph Legend
        direction LR
        _db[(Database/ConfigMap)] ~~~ _svc([Action/Process]) ~~~ _ext{{External System}}
    end
Loading
High-Level Assessment

The PR correctly builds on the prior Fulcio PKCS#11 implementation, reusing PKCS11InitContainerSpec, the ensureVolumeDefaultMode fix, and the in-place init-container reconciliation pattern to avoid infinite reconcile loops. Using a vendor-agnostic init-container contract (rather than baking a specific HSM vendor SDK into the operator image) is the right tradeoff for supporting arbitrary HSMs without operator changes per vendor. No materially better alternative architecture was evident from the diff.

Files changed (28) +8117 / -204

Enhancement (18) +1026 / -31
ctlog_types.goAdd CTlogSignerType, CTlogPKCS11Config and status types +87/-0

Add CTlogSignerType, CTlogPKCS11Config and status types

• Introduces the pkcs11/file signer enum, CTlogPKCS11Config spec type (reusing PKCS11InitContainerSpec), CTlogPKCS11Status, and 4 new CEL mutual-exclusion validation rules on CTlogSpec.

api/v1/ctlog_types.go

fulcio_types.goAdd CAType, PKCS11InitContainerSpec and FulcioPKCS11Config types +112/-2

Add CAType, PKCS11InitContainerSpec and FulcioPKCS11Config types

• Adds the CAType enum, curated PKCS11InitContainerSpec (shared with CTLog), PKCS11KeyConfig, FulcioPKCS11Config/Status, and new CEL validation rules for pkcs11 CA mode.

api/v1/fulcio_types.go

conversion_overrides.goManual conversion overrides for FulcioCert/FulcioStatus +27/-0

Manual conversion overrides for FulcioCert/FulcioStatus

• Adds manual Convert_* wrapper functions for FulcioCert and FulcioStatus so CAType/PKCS11 fields (v1-only) can be preserved/dropped explicitly during conversion.

api/v1alpha1/conversion_overrides.go

ctlog_conversion.goPreserve SignerType/PKCS11 fields via MarshalData in CTlog ConvertTo +3/-0

Preserve SignerType/PKCS11 fields via MarshalData in CTlog ConvertTo

• Restores Spec.SignerType, Spec.PKCS11 and Status.PKCS11 from the MarshalData annotation when converting v1alpha1 back to v1.

api/v1alpha1/ctlog_conversion.go

fulcio_conversion.goPreserve CAType/PKCS11 fields via MarshalData in Fulcio ConvertTo +3/-4

Preserve CAType/PKCS11 fields via MarshalData in Fulcio ConvertTo

• Removes the now-duplicated Convert_v1_FulcioStatus_To_v1alpha1_FulcioStatus (moved to conversion_overrides.go) and restores CAType/PKCS11 spec and status fields from annotations.

api/v1alpha1/fulcio_conversion.go

securesign_conversion.goPropagate Fulcio/CTlog PKCS#11 fields in Securesign conversion +4/-0

Propagate Fulcio/CTlog PKCS#11 fields in Securesign conversion

• Restores Fulcio.Certificate.CAType/PKCS11 and Ctlog.SignerType/PKCS11 from MarshalData when converting the aggregate Securesign resource.

api/v1alpha1/securesign_conversion.go

constants.goAdd PKCS11Condition and HSM constant names +19/-6

Add PKCS11Condition and HSM constant names

• Introduces PKCS11Condition and HSM init-container/volume/mount/env constants used by the new deployment and ensure-config actions; also renames monitor metrics port naming.

internal/controller/ctlog/actions/constants.go

deployment.goBranch CTLog deployment for PKCS#11 vs file signing +208/-0

Branch CTLog deployment for PKCS#11 vs file signing

• Adds ensurePKCS11Deployment and reconcileInitContainers to wire HSM init containers, volumes and --pkcs11_module_path arg, plus cleanup logic when switching back to file mode.

internal/controller/ctlog/actions/deployment.go

ensure_pkcs11_config.goNew EnsurePKCS11Config action for CTLog +207/-0

New EnsurePKCS11Config action for CTLog

• Validates pinSecretRef/publicKeyRef Secrets, populates Status.PKCS11 and Status.PublicKeyRef, and detects config drift to trigger key rotation and server config regeneration.

internal/controller/ctlog/actions/ensure_pkcs11_config.go

generate_signer.goSkip file-based signer secret generation in PKCS#11 mode +4/-0

Skip file-based signer secret generation in PKCS#11 mode

• Adds IsEnabled predicate so GenerateSignerAction is skipped entirely when signerType is pkcs11, since keys are HSM-managed.

internal/controller/ctlog/actions/generate_signer.go

server_config.goGenerate PKCS#11 protobuf server config for CTLog +68/-15

Generate PKCS#11 protobuf server config for CTLog

• Branches server config generation to buildPKCS11Config, reading PIN/public key from status refs and building keyspb.PKCS11Config; adds new annotations for PKCS#11 drift detection.

internal/controller/ctlog/actions/server_config.go

ctlog_controller.goRegister EnsurePKCS11ConfigAction and PKCS11Condition in reconciler +4/-0

Register EnsurePKCS11ConfigAction and PKCS11Condition in reconciler

• Adds the new action to the CTLog reconcile pipeline and conditionally appends PKCS11Condition to tracked conditions when signerType is pkcs11.

internal/controller/ctlog/ctlog_controller.go

ctlog_config.goAdd CreateCtlogPKCS11Config protobuf builder +61/-0

Add CreateCtlogPKCS11Config protobuf builder

• Builds a LogMultiConfig protobuf using keyspb.PKCS11Config for the private key instead of a PEM key file, then marshals it alongside root certs.

internal/controller/ctlog/utils/ctlog_config.go

constants.goAdd Fulcio PKCS11Condition and HSM path constants +17/-1

Add Fulcio PKCS11Condition and HSM path constants

• Introduces PKCS11Condition, CertPEMKey/CACrtKey and PKCS#11-related volume/mount path constants used by the new Fulcio HSM deployment path.

internal/controller/fulcio/actions/constants.go

ensure_pkcs11_config.goNew EnsurePKCS11Config action for Fulcio +139/-0

New EnsurePKCS11Config action for Fulcio

• Validates credentialsRef and pkcs11ConfigRef Secrets exist, populates Status.PKCS11, and detects drift to re-trigger reconciliation.

internal/controller/fulcio/actions/ensure_pkcs11_config.go

generate_signer.goAdapt signer resolution/status alignment for PKCS#11 CA mode +34/-2

Adapt signer resolution/status alignment for PKCS#11 CA mode

• resolveRef now requires caRef (not privateKeyRef) in pkcs11 mode, alignStatus clears private key refs and preserves caRef, and MutateSecret labels the secret using the CA key name in pkcs11 mode.

internal/controller/fulcio/actions/generate_signer.go

fulcio_controller.goRegister EnsurePKCS11ConfigAction and PKCS11Condition in Fulcio reconciler +5/-1

Register EnsurePKCS11ConfigAction and PKCS11Condition in Fulcio reconciler

• Adds the new action to the Fulcio reconcile pipeline and conditionally appends PKCS11Condition when CAType is pkcs11.

internal/controller/fulcio/fulcio_controller.go

config_map.goAdd FindConfigMap helper by label selector +24/-0

Add FindConfigMap helper by label selector

• New utility function retrieves a single ConfigMap matched by label, erroring on duplicates or not-found, using PartialObjectMetadata for efficiency.

internal/utils/kubernetes/config_map.go

Refactor (1) +465 / -141
deployment.goSplit Fulcio deployment into file-CA and PKCS#11-CA paths +465/-141

Split Fulcio deployment into file-CA and PKCS#11-CA paths

• Refactors ensureDeployment into ensureFileCADeployment and ensurePKCS11Deployment, adding init-container reconciliation, HSM/PKCS11 config/cert volumes, --ca=pkcs11ca args, and shared setProbes/ensureVolumeDefaultMode helpers.

internal/controller/fulcio/actions/deployment.go

Tests (3) +467 / -0
conversion_roundtrip_test.goAdd fuzzer constraints and tests for PKCS#11 roundtrip conversion +50/-0

Add fuzzer constraints and tests for PKCS#11 roundtrip conversion

• Adds ctlogPKCS11FuzzerFuncs/fulcioPKCS11FuzzerFuncs to constrain nested corev1 fields during fuzz round-trip tests, and wires them into existing Securesign/CTlog/Fulcio conversion tests.

api/v1alpha1/conversion_roundtrip_test.go

ensure_pkcs11_config_test.goUnit tests for Fulcio EnsurePKCS11Config action +245/-0

Unit tests for Fulcio EnsurePKCS11Config action

• Covers CanHandle state transitions (file mode, creating, drift) and Handle behavior for valid refs, missing secrets, and nil pkcs11 config.

internal/controller/fulcio/actions/ensure_pkcs11_config_test.go

fulcio_deployment_test.goUnit tests for PKCS#11 deployment volume defaulting +172/-0

Unit tests for PKCS#11 deployment volume defaulting

• Adds TestEnsureVolumeDefaultMode and TestPKCS11UserDefinedVolumesGetDefaultMode plus a createPKCS11Instance fixture for PKCS#11 Fulcio deployments.

internal/controller/fulcio/actions/fulcio_deployment_test.go

Documentation (1) +165 / -0
rhtas_v1_securesign_pkcs11.yamlAdd vendor-agnostic dual PKCS#11 sample CR +165/-0

Add vendor-agnostic dual PKCS#11 sample CR

• New sample Securesign CR demonstrating dual Fulcio+CTLog PKCS#11 HSM configuration with placeholder values and prerequisite Secret instructions.

config/samples/rhtas_v1_securesign_pkcs11.yaml

Other (5) +5994 / -32
zz_generated.deepcopy.goRegenerate deepcopy functions for new PKCS#11 types +245/-0

Regenerate deepcopy functions for new PKCS#11 types

• Adds generated DeepCopy/DeepCopyInto methods for CTlogPKCS11Config, CTlogPKCS11Status, FulcioPKCS11Config/Status and related types.

api/v1/zz_generated.deepcopy.go

zz_generated.conversion.goRegenerate conversion glue for new PKCS#11-aware manual converters +21/-30

Regenerate conversion glue for new PKCS#11-aware manual converters

• Moves FulcioCert/FulcioStatus conversion registration to AddConversionFunc (manual) and adds WARNING comments for fields requiring manual conversion (SignerType, PKCS11, CAType).

api/v1alpha1/zz_generated.conversion.go

rhtas.redhat.com_ctlogs.yamlAdd pkcs11 spec/status schema to CTlog CRD +2864/-0

Add pkcs11 spec/status schema to CTlog CRD

• Generates the OpenAPI schema for signerType, pkcs11 config (init containers, volumes, refs) and pkcs11 status on the CTlog CRD.

config/crd/bases/rhtas.redhat.com_ctlogs.yaml

rhtas.redhat.com_fulcios.yamlAdd caType/pkcs11 schema to Fulcio CRD +2829/-2

Add caType/pkcs11 schema to Fulcio CRD

• Generates the OpenAPI schema for caType, pkcs11 HSM config and pkcs11 status on the Fulcio CRD.

config/crd/bases/rhtas.redhat.com_fulcios.yaml

rhtas-operator.clusterserviceversion.yamlRegister v1alpha1 owned CRDs in CSV +35/-0

Register v1alpha1 owned CRDs in CSV

• Adds v1alpha1 versions of CTlog, Fulcio, Rekor, Securesign, TimestampAuthority, Trillian and Tuf to the operator's owned CRD list in the ClusterServiceVersion.

config/manifests/bases/rhtas-operator.clusterserviceversion.yaml

@codecov-commenter

codecov-commenter commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 40.78652% with 527 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.94%. Comparing base (9c019bb) to head (a9a8e53).

Files with missing lines Patch % Lines
api/v1/zz_generated.deepcopy.go 0.00% 113 Missing and 10 partials ⚠️
...l/controller/ctlog/actions/ensure_pkcs11_config.go 5.78% 114 Missing ⚠️
internal/controller/ctlog/actions/deployment.go 5.12% 108 Missing and 3 partials ⚠️
internal/controller/ctlog/utils/ctlog_config.go 0.00% 38 Missing ⚠️
.../controller/fulcio/actions/ensure_pkcs11_config.go 42.85% 33 Missing and 3 partials ⚠️
internal/controller/ctlog/actions/server_config.go 28.57% 30 Missing and 5 partials ⚠️
internal/controller/fulcio/actions/deployment.go 89.25% 20 Missing and 13 partials ⚠️
internal/utils/kubernetes/config_map.go 0.00% 15 Missing ⚠️
...ernal/controller/fulcio/actions/generate_signer.go 17.64% 11 Missing and 3 partials ⚠️
api/v1alpha1/zz_generated.conversion.go 0.00% 3 Missing and 1 partial ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2128      +/-   ##
==========================================
- Coverage   57.13%   55.94%   -1.19%     
==========================================
  Files         284      286       +2     
  Lines       15991    16735     +744     
==========================================
+ Hits         9136     9362     +226     
- Misses       5907     6389     +482     
- Partials      948      984      +36     
Flag Coverage Δ
e2e 64.80% <22.71%> (-4.62%) ⬇️
unit 35.17% <38.65%> (-0.32%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@qodo-for-securesign

qodo-for-securesign Bot commented Jul 23, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Auth secrets never mounted ✓ Resolved 🐞 Bug ≡ Correctness ⭐ New
Description
The new CTLog and Fulcio deployment paths copy Auth.Env but ignore Auth.SecretMount. HSM
integrations requiring file-mounted authentication credentials consequently start without those
credentials.
Code

internal/controller/ctlog/actions/deployment.go[R227-232]

+	if instance.Spec.Signer.Auth != nil {
+		for _, env := range instance.Spec.Signer.Auth.Env {
+			e := kubernetes.FindEnvByNameOrCreate(container, env.Name)
+			e.Value = env.Value
+			e.ValueFrom = env.ValueFrom
+		}
Relevance

⭐⭐⭐ High

PR 1143 established Auth.SecretMount deployment handling; omitting it conflicts with that accepted
implementation.

PR-#1143
PR-#1491

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The shared Auth API explicitly includes secret mounts, and the existing auth helper creates their
projected volume and mount. Both new deployment paths iterate only over Auth.Env, despite the new
sample also configuring secretMount.

api/v1/common.go[233-245]
internal/utils/kubernetes/ensure/auth.go[22-43]
internal/controller/ctlog/actions/deployment.go[226-233]
internal/controller/fulcio/actions/deployment.go[344-351]
config/samples/rhtas_v1_securesign_pkcs11.yaml[58-64]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Fulcio and CTLog process only the environment-variable portion of `Signer.Auth`, leaving `SecretMount` credentials out of the generated pod.

## Issue Context
The repository already provides `ensure.ContainerAuth`, which reconciles both auth environment variables and the projected auth secret volume and mount. Apply equivalent shared behavior in both PKCS#11 deployment paths, including cleanup when auth is removed.

## Fix Focus Areas
- internal/controller/ctlog/actions/deployment.go[226-233]
- internal/controller/fulcio/actions/deployment.go[344-351]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Persistence changes are ignored ✓ Resolved 🐞 Bug ≡ Correctness ⭐ New
Description
After the first reconciliation, CTLog only configures the existing hsm-tokens volume when it is
absent. Changing, adding, or removing pkcs11.persistence therefore leaves the Deployment using its
previous EmptyDir or PVC source.
Code

internal/controller/ctlog/actions/deployment.go[R261-272]

+	if !hasVolume(&template.Spec, HSMTokensVolumeName) {
+		tokensVol := kubernetes.FindVolumeByNameOrCreate(&template.Spec, HSMTokensVolumeName) //nolint:actionlint // template.Spec is the pod template, not the CR spec
+		// Clear previous VolumeSource before setting new one to prevent collision
+		tokensVol.VolumeSource = core.VolumeSource{}
+		if pkcs11Config.Persistence != nil && pkcs11Config.Persistence.Name != "" {
+			tokensVol.PersistentVolumeClaim = &core.PersistentVolumeClaimVolumeSource{
+				ClaimName: pkcs11Config.Persistence.Name,
+			}
+		} else {
+			tokensVol.EmptyDir = &core.EmptyDirVolumeSource{}
+		}
+	}
Relevance

⭐⭐⭐ High

PRs 1406 and 1574 demonstrate accepted reconciliation fixes when existing resources drift from
desired configuration.

PR-#1406
PR-#1574

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The deployment is reconciled in place through CreateOrUpdate, so hsm-tokens remains present
after initial creation. The persistence assignment is guarded by !hasVolume, preventing later
desired-state changes from being applied.

internal/controller/ctlog/actions/deployment.go[54-70]
internal/controller/ctlog/actions/deployment.go[259-272]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
CTLog only assigns the `hsm-tokens` VolumeSource when the volume does not already exist. Since reconciliation mutates the existing Deployment, subsequent persistence changes never replace the previous EmptyDir or PVC configuration.

## Issue Context
User-defined `hsm-tokens` volumes should still take precedence, but otherwise the operator-managed volume must be rebuilt from the current `pkcs11.persistence` value on every reconciliation.

## Fix Focus Areas
- internal/controller/ctlog/actions/deployment.go[251-272]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. PKCS11 module may be absent ✓ Resolved 🐞 Bug ☼ Reliability
Description
The CTlog deployment always sets --pkcs11_module_path under the shared hsm-lib volume, but the
operator only creates the hsm-lib-export init container (that copies the .so into that volume)
when spec.pkcs11.initContainers is non-empty; because initContainers is optional, a
valid-looking PKCS#11 CR can deploy with an emptyDir hsm-lib volume and no module present at the
configured path.
Code

internal/controller/ctlog/actions/deployment.go[R341-346]

+	if pkcs11ModulePath != "" && len(specs) > 0 {
+		desiredNames[HSMLibExportContainerName] = struct{}{}
+		libExport := kubernetes.FindInitContainerByNameOrCreate(podSpec, HSMLibExportContainerName)
+		libExport.Image = specs[0].Image
+		libExport.Command = []string{"cp", pkcs11ModulePath, fmt.Sprintf("%s/", HSMLibMountPath)}
+		libExport.VolumeMounts = []core.VolumeMount{
Relevance

⭐⭐⭐ High

Team usually accepts fixes preventing misconfigured deployments and missing optional-spec guards.

PR-#1243
PR-#1484
PR-#1406

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The API allows omitting initContainers, but the deployment logic still configures a module path
under an EmptyDir hsm-lib volume, and the .so copy step is skipped when the init container
list is empty.

api/v1/ctlog_types.go[115-146]
internal/controller/ctlog/actions/deployment.go[141-159]
internal/controller/ctlog/actions/deployment.go[261-279]
internal/controller/ctlog/actions/deployment.go[339-346]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
In PKCS#11 mode, the CTlog container is always configured with `--pkcs11_module_path` pointing into `/var/lib/hsm/lib/<basename>`. However, the operator only adds the `hsm-lib-export` init container that copies the PKCS#11 module into that shared volume when `len(spec.pkcs11.initContainers) > 0`. Since `initContainers` is currently optional in the API, this allows configurations where the module path is set but nothing populates the module file.

### Issue Context
- `hsm-lib` defaults to `EmptyDir` when not user-supplied, meaning it starts empty.
- `CreateCtlog` will still pass the module path flag regardless of whether the export init container exists.

### Fix Focus Areas
- api/v1/ctlog_types.go[115-146]
- internal/controller/ctlog/actions/deployment.go[151-159]
- internal/controller/ctlog/actions/deployment.go[261-279]
- internal/controller/ctlog/actions/deployment.go[339-349]

### Suggested fix options (pick one)
**Option A (recommended): enforce initContainers non-empty in PKCS#11 mode**
1. Add a CEL rule on `CTlogPKCS11Config` requiring `size(self.initContainers) > 0` (or at least when `pkcs11ModulePath` is set).
2. Add a defensive runtime check in `ensurePKCS11Config.Handle` (or in `ensureDeployment` before appending the flag) that returns a clear error/condition when `len(p.InitContainers) == 0`.

**Option B: make export container independent of user initContainers**
1. Introduce a dedicated field (e.g., `moduleImage`) to source the `.so` from, and always create `hsm-lib-export` when `pkcs11ModulePath` is set.

Also add a unit test covering PKCS#11 mode with empty initContainers to ensure the operator rejects it (Option A) or correctly populates the library (Option B).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. PKCS11 prefix hardcoded ✓ Resolved 🐞 Bug ≡ Correctness
Description
CreateCtlogPKCS11Config hardcodes LogConfig.Prefix to "trusted-artifact-signer" instead of using the
CTlog spec Prefix, so PKCS#11 mode will generate incorrect log config whenever a non-default prefix
is configured.
Code

internal/controller/ctlog/utils/ctlog_config.go[R201-205]

+	logConfig := configpb.LogConfig{
+		LogId:          treeID,
+		Prefix:         "trusted-artifact-signer",
+		RootsPemFile:   rootPems,
+		PrivateKey:     mustMarshalAny(pkcs11Key),
Relevance

⭐⭐⭐ High

Hardcoded config values vs CR spec is a clear correctness bug; similar spec-driven ctlog prefix
handling accepted.

PR-#1484
PR-#1406

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
File-mode config generation uses the CR’s Prefix value, but the new PKCS#11 config generator uses
a constant, so PKCS#11 mode diverges from the configured prefix.

internal/controller/ctlog/utils/ctlog_config.go[146-151]
internal/controller/ctlog/utils/ctlog_config.go[178-209]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`CreateCtlogPKCS11Config` hardcodes the CT log prefix (`LogConfig.Prefix`) to `"trusted-artifact-signer"`. In file-signer mode the prefix comes from `instance.Spec.Prefix`, so PKCS#11 deployments with a custom prefix will produce a config that does not match the CR’s requested prefix.

### Issue Context
- File-signer config generation wires the prefix through (`CreateCtlogConfig(..., logPrefix)`), but PKCS#11 config generation currently does not accept/propagate a prefix.

### Fix Focus Areas
- internal/controller/ctlog/utils/ctlog_config.go[178-210]
- internal/controller/ctlog/actions/server_config.go[144-166]

### Suggested fix
1. Add a `logPrefix string` parameter to `CreateCtlogPKCS11Config(...)`.
2. In `server_config.buildPKCS11Config`, pass `instance.Spec.Prefix` into the PKCS#11 config generator.
3. Set `logConfig.Prefix = logPrefix` (mirroring `CreateCtlogConfig`).
4. Add/adjust unit tests (if present) to cover a non-default prefix in PKCS#11 mode.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. CTLog auth is misplaced ✓ Resolved 🐞 Bug ≡ Correctness ⭐ New
Description
The new sample places CTLog auth beside signer, although the API defines it inside
CTlogSigner. A schema-validating API server therefore rejects the sample's unknown
spec.ctlog.auth field.
Code

config/samples/rhtas_v1_securesign_pkcs11.yaml[R97-103]

+    auth:
+      env:
+        - name: "<VENDOR_AUTH_ENV>"
+          value: "<vendor-auth-value>"
+      secretMount:
+        - name: "<your-auth-secret>"
+          key: "<auth-secret-key>"
Relevance

⭐⭐⭐ High

Sample correctness fixes were previously merged for malformed CTLog configuration and invalid CA
defaults.

PR-#1236
PR-#1149

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The sample ends the signer block before auth by placing both at the same indentation.
CTlogSigner owns the Auth field, while CTlogSpec has no top-level Auth field.

config/samples/rhtas_v1_securesign_pkcs11.yaml[85-104]
api/v1/ctlog_types.go[81-109]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PKCS#11 SecureSign sample nests CTLog `auth` at the CTLog specification level rather than within `signer`, making the example inconsistent with the API schema.

## Issue Context
Move the complete `auth` block under `spec.ctlog.signer`, alongside `type` and `pkcs11`.

## Fix Focus Areas
- config/samples/rhtas_v1_securesign_pkcs11.yaml[85-103]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Config key is not validated ✓ Resolved 🐞 Bug ☼ Reliability ⭐ New
Description
Fulcio marks PKCS#11 configuration resolved after checking only that the configRef Secret exists,
without verifying configRef.key. A missing key prevents the configuration file from being mounted
while status incorrectly reports successful validation.
Code

internal/controller/fulcio/actions/ensure_pkcs11_config.go[55]

+	exists, err := kubernetes.ExistsSecret(ctx, i.Client, instance.Namespace, pkcs11Config.ConfigRef.Name)
Relevance

⭐⭐⭐ High

PRs 1406 and 2131 accepted validating referenced secret contents before reporting configuration
resolved.

PR-#1406
PR-#2131

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
ExistsSecret checks only object existence, whereas the deployment constructs Fulcio's config path
from the selected key. The repository's GetSecretData helper demonstrates the required
name-and-key validation and is already used by CTLog's PKCS#11 action.

internal/controller/fulcio/actions/ensure_pkcs11_config.go[54-85]
internal/controller/fulcio/actions/deployment.go[305-314]
internal/utils/kubernetes/secret.go[34-47]
internal/controller/ctlog/actions/ensure_pkcs11_config.go[95-123]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Fulcio PKCS#11 prerequisite action validates only Secret metadata, not the data key selected by `configRef.key`. This allows an unusable configuration to receive a resolved condition.

## Issue Context
Use `GetSecretData` or an equivalent check that verifies both the Secret and selected key. Preserve the pending/error conditions when either is absent.

## Fix Focus Areas
- internal/controller/fulcio/actions/ensure_pkcs11_config.go[54-85]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Previous review results

Review updated until commit a9a8e53

Results up to commit 754718d ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. PKCS11 prefix hardcoded ✓ Resolved 🐞 Bug ≡ Correctness
Description
CreateCtlogPKCS11Config hardcodes LogConfig.Prefix to "trusted-artifact-signer" instead of using the
CTlog spec Prefix, so PKCS#11 mode will generate incorrect log config whenever a non-default prefix
is configured.
Code

internal/controller/ctlog/utils/ctlog_config.go[R201-205]

+	logConfig := configpb.LogConfig{
+		LogId:          treeID,
+		Prefix:         "trusted-artifact-signer",
+		RootsPemFile:   rootPems,
+		PrivateKey:     mustMarshalAny(pkcs11Key),
Relevance

⭐⭐⭐ High

Hardcoded config values vs CR spec is a clear correctness bug; similar spec-driven ctlog prefix
handling accepted.

PR-#1484
PR-#1406

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
File-mode config generation uses the CR’s Prefix value, but the new PKCS#11 config generator uses
a constant, so PKCS#11 mode diverges from the configured prefix.

internal/controller/ctlog/utils/ctlog_config.go[146-151]
internal/controller/ctlog/utils/ctlog_config.go[178-209]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`CreateCtlogPKCS11Config` hardcodes the CT log prefix (`LogConfig.Prefix`) to `"trusted-artifact-signer"`. In file-signer mode the prefix comes from `instance.Spec.Prefix`, so PKCS#11 deployments with a custom prefix will produce a config that does not match the CR’s requested prefix.

### Issue Context
- File-signer config generation wires the prefix through (`CreateCtlogConfig(..., logPrefix)`), but PKCS#11 config generation currently does not accept/propagate a prefix.

### Fix Focus Areas
- internal/controller/ctlog/utils/ctlog_config.go[178-210]
- internal/controller/ctlog/actions/server_config.go[144-166]

### Suggested fix
1. Add a `logPrefix string` parameter to `CreateCtlogPKCS11Config(...)`.
2. In `server_config.buildPKCS11Config`, pass `instance.Spec.Prefix` into the PKCS#11 config generator.
3. Set `logConfig.Prefix = logPrefix` (mirroring `CreateCtlogConfig`).
4. Add/adjust unit tests (if present) to cover a non-default prefix in PKCS#11 mode.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. PKCS11 module may be absent ✓ Resolved 🐞 Bug ☼ Reliability
Description
The CTlog deployment always sets --pkcs11_module_path under the shared hsm-lib volume, but the
operator only creates the hsm-lib-export init container (that copies the .so into that volume)
when spec.pkcs11.initContainers is non-empty; because initContainers is optional, a
valid-looking PKCS#11 CR can deploy with an emptyDir hsm-lib volume and no module present at the
configured path.
Code

internal/controller/ctlog/actions/deployment.go[R341-346]

+	if pkcs11ModulePath != "" && len(specs) > 0 {
+		desiredNames[HSMLibExportContainerName] = struct{}{}
+		libExport := kubernetes.FindInitContainerByNameOrCreate(podSpec, HSMLibExportContainerName)
+		libExport.Image = specs[0].Image
+		libExport.Command = []string{"cp", pkcs11ModulePath, fmt.Sprintf("%s/", HSMLibMountPath)}
+		libExport.VolumeMounts = []core.VolumeMount{
Relevance

⭐⭐⭐ High

Team usually accepts fixes preventing misconfigured deployments and missing optional-spec guards.

PR-#1243
PR-#1484
PR-#1406

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The API allows omitting initContainers, but the deployment logic still configures a module path
under an EmptyDir hsm-lib volume, and the .so copy step is skipped when the init container
list is empty.

api/v1/ctlog_types.go[115-146]
internal/controller/ctlog/actions/deployment.go[141-159]
internal/controller/ctlog/actions/deployment.go[261-279]
internal/controller/ctlog/actions/deployment.go[339-346]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
In PKCS#11 mode, the CTlog container is always configured with `--pkcs11_module_path` pointing into `/var/lib/hsm/lib/<basename>`. However, the operator only adds the `hsm-lib-export` init container that copies the PKCS#11 module into that shared volume when `len(spec.pkcs11.initContainers) > 0`. Since `initContainers` is currently optional in the API, this allows configurations where the module path is set but nothing populates the module file.

### Issue Context
- `hsm-lib` defaults to `EmptyDir` when not user-supplied, meaning it starts empty.
- `CreateCtlog` will still pass the module path flag regardless of whether the export init container exists.

### Fix Focus Areas
- api/v1/ctlog_types.go[115-146]
- internal/controller/ctlog/actions/deployment.go[151-159]
- internal/controller/ctlog/actions/deployment.go[261-279]
- internal/controller/ctlog/actions/deployment.go[339-349]

### Suggested fix options (pick one)
**Option A (recommended): enforce initContainers non-empty in PKCS#11 mode**
1. Add a CEL rule on `CTlogPKCS11Config` requiring `size(self.initContainers) > 0` (or at least when `pkcs11ModulePath` is set).
2. Add a defensive runtime check in `ensurePKCS11Config.Handle` (or in `ensureDeployment` before appending the flag) that returns a clear error/condition when `len(p.InitContainers) == 0`.

**Option B: make export container independent of user initContainers**
1. Introduce a dedicated field (e.g., `moduleImage`) to source the `.so` from, and always create `hsm-lib-export` when `pkcs11ModulePath` is set.

Also add a unit test covering PKCS#11 mode with empty initContainers to ensure the operator rejects it (Option A) or correctly populates the library (Option B).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

Comment thread internal/controller/ctlog/utils/ctlog_config.go
Comment thread internal/controller/ctlog/actions/deployment.go Outdated
@sampras343

Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-for-securesign

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8bb1ae8

@sampras343
sampras343 force-pushed the sachin/feat/pkcs11-v2-ctlog branch 2 times, most recently from acd89d3 to 0447898 Compare July 24, 2026 08:45
@sampras343
sampras343 requested a review from osmman July 24, 2026 10:14
@sampras343
sampras343 force-pushed the sachin/feat/pkcs11-v2-ctlog branch from 0447898 to b404dbf Compare July 27, 2026 09:08
@sampras343
sampras343 force-pushed the sachin/feat/pkcs11-v2-ctlog branch 2 times, most recently from 8d68073 to 053afe7 Compare July 27, 2026 09:30

@osmman osmman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Request changes — API isn't released yet, let's fix the shape now.

  1. Reuse Auth instead of CredentialsRef/ServerEnv. We already have this pattern (Rekor/TSA/Trillian) — add Auth on FulcioCert, drop CredentialsRef/ServerEnv. Exception: CTLog's PinSecretRef stays, since the operator reads that value itself to build ct_server's config. Also: don't auto-inject the PIN into every init container — most don't need it, let users add it explicitly where they do.

  2. Move InitContainers/Volumes/ServerVolumeMounts out of pkcs11 config to FulcioSpec/CTlogSpec top level. These are pod-wide regardless of nesting, and not PKCS#11-specific. Rename ServerVolumeMountsVolumeMounts while moving it (no longer needs the "Server" qualifier once it's not sitting next to per-init-container mounts).

  3. Drop the "at least one initContainer required for pkcs11" CEL rule. It assumes one specific way of provisioning the HSM library — other valid approaches exist (CSI driver, mutating webhook) that wouldn't populate initContainers at all. Let pod startup failure be the real signal if provisioning didn't work.

  4. Drop CTlogPKCS11Status/FulcioPKCS11Status entirely. Every field is a required spec value copied verbatim — the operator never generates or modifies any of them, so they don't belong in status. Their driftDetected/hasPKCS11ConfigDrift checks are redundant too (ObservedGeneration already covers it).

  5. (Discuss) Consider spec.signer shape, matching Rekor/TSA. Fulcio uses certificate, CTLog uses flat fields — worth converging while unreleased. Bigger diff than 1-4, especially for Fulcio — fine as a fast-follow if too much for this PR.

Happy to go through any of these in more detail — the reasoning behind them.

@sampras343
sampras343 force-pushed the sachin/feat/pkcs11-v2-ctlog branch from 053afe7 to f65b798 Compare July 28, 2026 15:46
Enable the operator to deploy Fulcio and CTLog with HSM-backed
signing keys via PKCS#11, using a vendor-agnostic init container
plugin model. Integrates cleanly into the spec.signer hierarchy.

Fulcio PKCS#11 (spec.fulcio.signer):
- type: pkcs11 on FulcioSigner with CEL mutual-exclusion rules
- FulcioPKCS11Config: configRef (crypto11.conf) + keyConfig (HSM key ID)
- signer.auth (Auth struct) for server container env vars
- signer.certificateChain.certificateChainRef for pre-provisioned root CA
- Top-level initContainers/volumes/volumeMounts on FulcioSpec

CTLog PKCS#11 (spec.ctlog.signer):
- type: pkcs11 on CTlogSigner with CEL mutual-exclusion rules
- CTlogPKCS11Config: pinSecretRef, publicKeyRef, tokenLabel, modulePath
- signer.auth for server container env vars
- Config-driven dispatch: keyspb.PKCS11Config in protobuf config
- Top-level initContainers/volumes/volumeMounts on CTlogSpec
- Status.PublicKeyRef populated for trust material resolution

Design decisions (per review feedback):
- Reuse Auth struct instead of custom CredentialsRef/ServerEnv
- Move initContainers/volumes/volumeMounts to top-level spec (pod-wide)
- Drop PKCS11 status structs and drift detection (ObservedGeneration)
- Rename configRef/modulePath (drop redundant PKCS11 prefix)
- Drop initContainers CEL rule (allow CSI/webhook provisioning)
- Stop auto-injecting HSM_PIN into init containers
- Auth on Signer (uniform for Fulcio and CTLog)
- Call Signer.SetDefaults() from SecureSign.SetDefaults() so
  signer.type: file is explicit on the parent CR

Shared:
- PKCS11InitContainerSpec: curated corev1.Container subset
- v1alpha1 conversion preserves PKCS#11 fields via MarshalData

E2E validated on OCP 4.22: dual PKCS#11 cosign sign+verify PASS,
file mode regression PASS.

Jira: SECURESIGN-5014

Signed-off-by: Sachin Sampras M <sacm@redhat.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sampras343
sampras343 force-pushed the sachin/feat/pkcs11-v2-ctlog branch from f65b798 to 71a6670 Compare July 28, 2026 15:53

@sampras343 sampras343 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

All five points addressed in the latest push (71a6670a). Here is how each was resolved:

1. Reuse Auth instead of CredentialsRef/ServerEnv

Done. CredentialsRef and ServerEnv removed from both FulcioPKCS11Config and CTlogPKCS11Config. Auth *Auth added on FulcioSigner and CTlogSigner (uniform placement on both — uses the existing Auth{Env, SecretMount} type from common.go). CTLog PinSecretRef stays as requested — operator reads it server-side to embed in the protobuf config. HSM_PIN auto-injection into init containers removed (HSMPinEnvVar constant deleted) — users set it explicitly in initContainers[].env if their vendor image needs it. Auth env vars injected idempotently using FindEnvByNameOrCreate to prevent duplicate env vars on reconciliation.

2. Move InitContainers/Volumes/ServerVolumeMounts to top-level

Done. All three fields moved from FulcioPKCS11Config/CTlogPKCS11Config to FulcioSpec/CTlogSpec. ServerVolumeMounts renamed to VolumeMounts. Not placed on PodRequirements — only Fulcio/CTLog get these fields. Controller code updated to read from instance.Spec.InitContainers/instance.Spec.Volumes/instance.Spec.VolumeMounts instead of pkcs11Config.*.

3. Drop the initContainers CEL rule

Done. The has(self.initContainers) && size(self.initContainers) > 0 rule removed from CTlogPKCS11Config. The initContainers field changed back from required to optional. Vault Agent Injector, CSI drivers, and cert-manager can now provision the PKCS#11 library without declaring init containers in the CR.

4. Drop CTlogPKCS11Status/FulcioPKCS11Status

Done. Both status structs deleted. Status.PKCS11 field removed from CTlogStatus and FulcioStatus. hasPKCS11ConfigDrift/driftDetected functions deleted — ObservedGeneration check in CanHandle handles all spec changes. Status.PublicKeyRef (on CTlogStatus directly, not on the deleted PKCS11 sub-struct) retained for trust material resolution. All conversion code updated — Status.PKCS11 restoration lines removed.

5. spec.signer shape

Done in this PR (not deferred). Both Fulcio and CTLog now use the spec.signer hierarchy introduced by the signer restructure PRs (#2155, #2156). PKCS#11 config is at signer.pkcs11, type selector is signer.type: pkcs11. Additionally, SecureSign.SetDefaults() now calls Signer.SetDefaults() for both Fulcio and CTLog, so signer.type: file is explicit on the parent CR — CEL rules do not need !has(self.type) guards.

Also fixed from Qodo review:

  • Hardcoded CTLog prefix in CreateCtlogPKCS11Config → now accepts logPrefix parameter
  • Field renames: pkcs11ConfigRefconfigRef, pkcs11ModulePathmodulePath (already scoped under pkcs11:)

@sampras343

Copy link
Copy Markdown
Member Author

/retest

@sampras343

Copy link
Copy Markdown
Member Author

/agentic_review

Comment thread internal/controller/ctlog/actions/deployment.go
Comment thread internal/controller/ctlog/actions/deployment.go
@qodo-for-securesign

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 71a6670

@sampras343
sampras343 force-pushed the sachin/feat/pkcs11-v2-ctlog branch from 54c3f35 to eadd047 Compare July 28, 2026 20:55
@sampras343

Copy link
Copy Markdown
Member Author

/retest

@sampras343
sampras343 requested a review from osmman July 29, 2026 08:14
@sampras343
sampras343 force-pushed the sachin/feat/pkcs11-v2-ctlog branch from eadd047 to 3dba406 Compare July 29, 2026 09:19
@sampras343

Copy link
Copy Markdown
Member Author

Qodo findings #5 and #6 addressed in 3dba406:

#5. CTLog auth misplaced in sample CR — Fixed. The sample CR had auth: at spec.ctlog.auth (top-level) instead of spec.ctlog.signer.auth (inside CTlogSigner). Moved to correct location matching the API definition. The API server would have rejected the previous sample.

#6. Config key not validated — Fixed. Replaced ExistsSecret (checks Secret name only) with GetSecretData (reads the actual key from the Secret). If the Secret exists but the specified key is missing, the operator now reports a clear error: configRef Secret "name" key "key" not accessible instead of silently marking the config as resolved while the volume mount would be empty.

- CTLog PIN content hashing for rotation detection
- ObservedGeneration on all PKCS11 error conditions
- Fulcio hsm-lib ReadOnly, rotation handling
- Auth SecretMount via ensure.ContainerAuth
- CTLog persistence idempotency
- modulePath CRD validation (.so pattern)

Signed-off-by: Sachin Sampras M <sampras343@gmail.com>
@sampras343
sampras343 force-pushed the sachin/feat/pkcs11-v2-ctlog branch from 3dba406 to a9a8e53 Compare July 29, 2026 09:27

@osmman osmman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

API-redesign feedback is fully addressed. Remaining issues inline — headlines:

Blocking:

  • PKCS#11 "rotation" fires on any spec change, not just PKCS#11 fields — spurious config regen + pod restarts (Fulcio + CTLog).
  • InitContainers/Volumes/VolumeMounts/Auth are silent no-ops outside type: pkcs11, despite being moved to spec top level to be signer-agnostic.
  • Persistence on CTlogPKCS11Config doesn't provision a PVC and silently overwrites a user-defined hsm-tokens volume every reconcile — conflicts with the new Volumes mechanism this PR introduced.
  • No CTLog unit tests, no controller/e2e coverage — would have caught the bugs above.

Worth fixing: fragile != pkcs11 check in generate_signer.go; duplicated/drifting helpers between Fulcio and CTLog; roundtrip fuzzer skips testing the restore path it already has.

Recommendation: split this PR

Too large (~15k lines) to review the PKCS#11 logic as deeply as it needs. Please split into:

  1. Generic InitContainers/Volumes/VolumeMounts/Auth extension, signer-agnostic.
  2. PKCS#11 signer support on top of (1).

Would also make proper test coverage realistic for the PKCS#11 piece.

return true
}
// Fire if CR generation changed (e.g., tokenLabel updated)
if cond.ObservedGeneration != instance.GetGeneration() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PKCS#11 "rotation" fires on any spec change, not just PKCS#11 field changes. CanHandle() returns true whenever ObservedGeneration != Generation — true for any spec edit (resources, TLS, monitoring, etc.), not just a PKCS#11 field change. Combined with Handle() unconditionally calling handleRotation() once PKCS11Condition is True, any unrelated spec bump on a PKCS#11-mode CTlog regenerates the server config Secret (new GenerateName) and restarts the pod. Consider comparing the actual PKCS#11 fields (tokenLabel, modulePath, configRef, etc.) against a stored/observed value before triggering rotation. Same gap in fulcio/actions/ensure_pkcs11_config.go.

if cond == nil {
return true
}
if cond.ObservedGeneration != instance.GetGeneration() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same gap as CTLog's ensure_pkcs11_config.go: this only checks ObservedGeneration != Generation, which fires on any spec change, not specifically a PKCS#11 field change. Handle() then calls handleRotation() whenever PKCS11Condition is already True — so any unrelated Fulcio spec edit regenerates the server config Secret and restarts the pod.

// or omitted entirely if the custom Fulcio image bundles the vendor SDK).
if !hasVolume(&template.Spec, HSMTokensVolumeName) {
hsmTokensVol := kubernetes.FindVolumeByNameOrCreate(&template.Spec, HSMTokensVolumeName)
hsmTokensVol.EmptyDir = &core.EmptyDirVolumeSource{}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Drift vs. CTLog's equivalent path. Unlike ctlog/actions/deployment.go (which resets tokensVol.VolumeSource = core.VolumeSource{} before assigning EmptyDir/PVC), this sets EmptyDir/PersistentVolumeClaim directly without clearing the existing VolumeSource first — if a volume switches source type across reconciles, stale fields could linger. Also: hasVolume/hasMountPath/ensureVolumeDefaultMode (lines 538-568 below) are copy-pasted verbatim from the CTLog package; consider moving them into internal/utils/kubernetes next to FindVolumeByNameOrCreate so both stay in sync.

}

// Validate required refs
if p.PinSecretRef == nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The PinSecretRef nil-check+error (61-77) and PublicKeyRef nil-check+error (79-95) blocks are near-identical, as are the two GetSecretData validation blocks below (98-134). Worth collapsing into a small validateSecretRef(...)-style helper to cut the duplication.

"/var/run/fulcio-secrets/cert.pem",
fmt.Sprintf("--ct-log-url=%s", ctlogUrl),
}
func (i deployAction) ensureFileCADeployment(instance *rhtasv1.Fulcio, sa string, labels map[string]string, dp *v1.Deployment) error {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ensureFileCADeployment and ensurePKCS11Deployment (line 291) both re-derive the same Replicas/Selector/template labels/ServiceAccountName/AutomountServiceAccountToken, http/grpc/monitoring port setup, and the fulcio-config/oidc-info volume+mount wiring. Consider extracting the shared pod scaffolding into a helper (similar to the existing setProbes) so the two paths can't silently diverge.

}
}

func (i deployAction) ensurePKCS11Deployment(instance *rhtasv1.CTlog, template *core.PodTemplateSpec, container *core.Container) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd prefer removing the //nolint:actionlint suppressions below rather than keeping them. InitContainers/Volumes/VolumeMounts/Auth are cross-cutting pod concerns, not CTLog-specific — internal/utils/kubernetes/ensure already exists for exactly this (see ensure.ContainerAuth, used two lines below). Adding ensure.InitContainers(...)/ensure.Volumes(...)/ensure.VolumeMounts(...) there, alongside ContainerAuth, and calling them from both Fulcio and CTLog would remove the duplication flagged above and the nolint noise as a side effect (free functions in ensure aren't methods on an action-type receiver, so the analyzer never looks at them).

AlignStatus: alignStatus,
IsEnabled: func(i *rhtasv1.CTlog) bool {
// PKCS#11 mode manages keys on the HSM — no file-based signer secret needed.
return i.Spec.Signer.Type != rhtasv1.CTlogSignerTypePKCS11

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Negative check on signer type is fragile for future signer types. IsEnabled returns true whenever Type != CTlogSignerTypePKCS11 — today that's equivalent to Type == CTlogSignerTypeFile since those are the only two types, but it silently breaks the moment a third type (e.g. kms) is added: this would then generate/manage a file-based private key secret for a KMS-backed signer too, which is wrong. Please check Type == rhtasv1.CTlogSignerTypeFile instead, so unknown/future types fail closed (disabled) rather than fail open (enabled). Fulcio's generate_signer.go avoids this by not needing an IsEnabled gate at all and checking == FulcioSignerTypePKCS11 positively wherever it branches — worth mirroring that style here.


// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch

func NewEnsurePKCS11ConfigAction() action.Action[*rhtasv1.CTlog] {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Missing test coverage: no unit tests for CTLog's PKCS#11 support, and no integration/e2e coverage for the feature at all. This PR adds unit tests for Fulcio's PKCS#11 pieces (fulcio/actions/ensure_pkcs11_config_test.go, fulcio/actions/fulcio_deployment_test.go) but this file and ctlog/actions/deployment.go have none. More importantly, every test added here (including the Fulcio ones) exercises a single action in isolation — nothing runs the full reconcile flow through the existing envtest infra (ctlog_controller_test.go/fulcio_controller_test.go, testonly.ControllerSuite), and there's no test/e2e coverage either. This is a multi-action state machine (config validation → server config generation → deployment, with rotation/drift handling) — at least one controller-level test that creates a signer.type: pkcs11 CR and asserts it reaches Ready with the expected init containers/volumes/mounts would exercise the interactions between actions that unit tests can't, and would have caught the rotation false-trigger and the ignored-outside-PKCS11-mode bugs flagged elsewhere in this review.

Comment thread api/v1/ctlog_types.go
// Persistent storage for HSM tokens (key survives pod restarts).
// When nil, an emptyDir is used (key is regenerated on pod restart).
//+optional
Persistence *Pvc `json:"persistence,omitempty"`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Persistence *Pvc doesn't do what the shared Pvc type implies, and duplicates the generic Volumes/VolumeMounts mechanism you already added. Unlike Rekor/TUF/Trillian's own pvc.go actions (which actually provision a PersistentVolumeClaim using Size/StorageClass/Retain), there's no equivalent action for CTLog — internal/controller/ctlog/actions/deployment.go only ever reads Persistence.Name and plugs it into PersistentVolumeClaimVolumeSource{ClaimName: ...} (lines 263-265, 274-276). Size, StorageClass, Retain (and their CEL immutability rules on Pvc) are dead for this field — a user setting them would reasonably expect the operator to provision storage, and it won't. Since this is just "reference an existing PVC by name," it seems fully replaceable by having the user define their own hsm-tokens volume via the generic spec.ctlog.volumes/volumeMounts you already introduced, with no dedicated Persistence field needed at all.

That also surfaces a real bug in the current precedence: the comment at deployment.go:248-249 says user-defined volumes are processed first "so operator-managed volumes take precedence for reserved names," but both the if !hasVolume(...) and else branches (256-280) unconditionally do tokensVol.VolumeSource = core.VolumeSource{} and rebuild it from Persistence/EmptyDir — so a user-defined hsm-tokens volume (e.g. a different PVC, or even a Secret) gets silently discarded and replaced every reconcile, regardless of what they configured. The two branches are functionally identical, so the hasVolume check isn't actually gating anything today.

#
# Replace all <placeholder> values with your environment-specific configuration.
# This sample is vendor-agnostic. For SoftHSM dev/test or production HSM examples,
# see docs/pkcs11-hsm-support.md.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This sample is 172 lines — by far the largest in config/samples/ (every other one is 9-73 lines). Files here are meant to feed the CSV's alm-examples annotation (see config/samples/kustomization.yaml: "Append samples you want in your CSV to this file as resources"), which gets stored as CR data in etcd — we intentionally keep these small. This one isn't wired into kustomization.yaml yet, but it shouldn't live here at all: please move it under docs/ and link it from documentation instead, matching the pattern of fulcio-key-rotation.md/ctlog-key-rotation.md/etc. Also, this comment already points at docs/pkcs11-hsm-support.md, but that file doesn't exist anywhere in the repo — please add it (or fix the reference) as part of the move.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants