Skip to content

Detect SCC UID/GID-range mismatch on namespace restore#449

Open
kaovilai wants to merge 1 commit into
openshift:oadp-devfrom
kaovilai:issue-448-scc-uid-gid-mismatch
Open

Detect SCC UID/GID-range mismatch on namespace restore#449
kaovilai wants to merge 1 commit into
openshift:oadp-devfrom
kaovilai:issue-448-scc-uid-gid-mismatch

Conversation

@kaovilai

@kaovilai kaovilai commented Jul 21, 2026

Copy link
Copy Markdown
Member

Note

Responses generated with Claude

Summary

Restoring into a namespace whose OpenShift-assigned UID/GID range differs from the one recorded at backup time silently breaks file ownership for workloads that chown'd data to the backup-time UID (e.g. non-root DB containers). Restore reports Completed; the failure only surfaces later as an app-level permission error. This is a documented OADP limitation, never implemented as code (see #448).

Why not a RestoreItemAction on namespaces (as originally proposed in #448)

Traced through the pinned github.com/openshift/velero fork's actual source: pkg/restore/restore.go unconditionally skips Namespace objects from ever reaching the RestoreItemAction pipeline —

// We don't want to explicitly restore namespace API objs because we'll handle
// them as a special case prior to restoring anything into them
if groupResource == kuberesource.Namespaces {
    continue
}

Namespace creation/existence is instead handled by a hardcoded kube.EnsureNamespaceExistsAndIsReady() call that reads the Namespace straight from the backup archive, with no plugin hook. A RestoreItemAction{AppliesTo: {"namespaces"}} would simply never fire on this Velero version.

Also confirmed: RestoreItemActionExecuteOutput has no Warning field in this API version — a plugin can only fail an item (blocking) or succeed silently, there's no "log a warning but still restore" return path for RestoreItemAction.

Approach

New package velero-plugins/namespacescc, piggybacking on serviceaccounts (always present in every namespace, already routes through the restore action pipeline):

  • backup.go: stashes the namespace's openshift.io/sa.scc.uid-range, sa.scc.supplemental-groups, and sa.scc.mcs annotations onto each backed-up ServiceAccount, under bookkeeping annotation keys (oadp.openshift.io/backup-ns-scc-*).
  • restore.go: reads those stashed values back from the pristine ItemFromBackup, resolves the actual target namespace (honoring Restore.Spec.NamespaceMapping), fetches its live annotations, and Log.Warnfs on any mismatch — the closest available signal given the API constraint above. Strips the bookkeeping annotations before the ServiceAccount is persisted, so they don't leak onto the restored object.
  • Both cache namespace annotation lookups per backup/restore name, so a namespace with multiple service accounts only costs one Namespaces().Get() call.

Explicitly out of scope: auto-reapplying the original range on mismatch (issue's stretch goal) — re-assigning a UID/GID range that OpenShift's allocator already gave to a different namespace risks a collision; needs its own design.

Real-world validation

Traced OpenShift's actual NamespaceSCCAllocationController (openshift/cluster-policy-controller): it only allocates a fresh range when the uid-range annotation is absent on the namespace — if present (e.g. because it pre-existed with its own, different range before Velero's EnsureNamespaceExistsAndIsReady no-op'd on it), the controller never re-validates or reconciles it. This matches the customer scenario that motivated #448 (restored PostgreSQL: chmod: ... Operation not permitted) — most likely triggered by restoring into a namespace pre-provisioned by cluster automation before the restore ran.

Fixes #448

Test plan

  • go build ./...
  • go vet ./velero-plugins/namespacescc/...
  • go test ./velero-plugins/namespacescc/... — new table-driven unit tests for the pure annotation stash/compare/strip helpers
  • Manual: restore into a namespace with a different sa.scc.uid-range than at backup time, confirm the plugin logs the mismatch

Summary by CodeRabbit

  • New Features

    • Added backup and restore handling for namespace security context constraint (SCC) settings captured on ServiceAccounts.
    • During backup, SCC range-related annotations from the parent namespace are preserved on ServiceAccounts for later validation.
    • During restore, the plugin checks for SCC annotation mismatches against current namespace settings, warns on differences, and removes the temporary bookkeeping metadata.
  • Tests

    • Added unit tests covering plugin selection, SCC annotation stashing/restoration logic, mismatch detection messaging, and cleanup behavior.

@openshift-ci openshift-ci Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 21, 2026
@openshift-ci

openshift-ci Bot commented Jul 21, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: kaovilai

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 2a3f50d0-9783-4dd9-9ae1-eec986a6a389

📥 Commits

Reviewing files that changed from the base of the PR and between a4117e5 and d597b27.

📒 Files selected for processing (6)
  • velero-plugins/common/types.go
  • velero-plugins/main.go
  • velero-plugins/namespacescc/backup.go
  • velero-plugins/namespacescc/backup_test.go
  • velero-plugins/namespacescc/restore.go
  • velero-plugins/namespacescc/restore_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • velero-plugins/namespacescc/backup_test.go
  • velero-plugins/namespacescc/restore_test.go
  • velero-plugins/namespacescc/restore.go
  • velero-plugins/namespacescc/backup.go

Walkthrough

Adds namespace SCC annotation bookkeeping during ServiceAccount backup and verifies restored namespace annotations for mismatches. New backup and restore item actions are registered with caching, warning logs, cleanup, and unit tests.

Changes

Namespace SCC range handling

Layer / File(s) Summary
Backup SCC annotation capture
velero-plugins/common/types.go, velero-plugins/namespacescc/backup.go, velero-plugins/namespacescc/backup_test.go
Defines SCC bookkeeping keys and copies namespace SCC annotations onto backed-up ServiceAccounts using per-backup namespace caching.
Restore SCC mismatch verification
velero-plugins/namespacescc/restore.go, velero-plugins/namespacescc/restore_test.go
Compares backed-up SCC values with restored namespace annotations, logs mismatches, removes bookkeeping annotations, and tests the helper behavior.
Plugin registration
velero-plugins/main.go
Registers the namespace SCC backup and restore item actions under 02-namespacescc.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ServiceAccount
  participant BackupPlugin
  participant KubernetesAPI
  ServiceAccount->>BackupPlugin: backup service account
  BackupPlugin->>KubernetesAPI: fetch namespace SCC annotations
  KubernetesAPI-->>BackupPlugin: return annotation values
  BackupPlugin-->>ServiceAccount: add bookkeeping annotations
Loading
sequenceDiagram
  participant RestoreItemAction
  participant RestorePlugin
  participant KubernetesAPI
  participant RestoreLog
  RestoreItemAction->>RestorePlugin: restore ServiceAccount
  RestorePlugin->>KubernetesAPI: fetch target namespace annotations
  KubernetesAPI-->>RestorePlugin: return current values
  RestorePlugin->>RestoreLog: log SCC mismatch warnings
  RestorePlugin-->>RestoreItemAction: return cleaned ServiceAccount
Loading

Suggested reviewers: sseago, shubham-pampattiwar


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
No-Sensitive-Data-In-Logs ❌ Error Restore warnings log the target namespace plus backed-up/restored SCC annotation values, exposing customer-specific cluster details in logs. Redact namespace names and annotation values, or log only that an SCC mismatch occurred without the concrete expected/actual data.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (13 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: detecting SCC UID/GID-range mismatches during namespace restore.
Linked Issues check ✅ Passed The PR implements restore-time SCC mismatch detection and warnings using ServiceAccount proxy handling, matching the issue's diagnostic goal.
Out of Scope Changes check ✅ Passed The changes stay focused on SCC annotation backup and restore mismatch detection without introducing unrelated functionality.
Stable And Deterministic Test Names ✅ Passed All new test titles are static strings; no timestamps, random IDs, pod/node/namespace suffixes, or fmt-generated names appear.
Test Structure And Quality ✅ Passed PASS: The new tests are pure table-driven unit tests, not Ginkgo; they need no cluster setup/cleanup or timeouts and match existing repo patterns.
Microshift Test Compatibility ✅ Passed The new tests are plain Go unit tests, not Ginkgo e2e specs, and they don’t reference any MicroShift-unsupported OpenShift APIs or features.
Single Node Openshift (Sno) Test Compatibility ✅ Passed Added tests are plain Go unit tests in namespacescc; no Ginkgo e2e specs or SNO/multi-node assumptions were found.
Topology-Aware Scheduling Compatibility ✅ Passed PR only adds namespace-SCC backup/restore plugins and constants; no deployments, replicas, affinities, nodeSelectors, or topology-spread constraints were introduced.
Ote Binary Stdout Contract ✅ Passed No new process-level stdout writes were added; main/init paths have no print calls, and namespacescc only adds logger-backed plugin methods.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; the new tests are standard Go unit tests and show no IPv4-only or external-network assumptions.
No-Weak-Crypto ✅ Passed No MD5/SHA1/DES/RC4/3DES/Blowfish/ECB, custom crypto, or secret/token comparisons were added; new code only compares SCC annotations and namespaces.
Container-Privileges ✅ Passed PR changes only Go source/tests; no manifest files or privileged/host* /allowPrivilegeEscalation/securityContext settings were added.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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.

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@velero-plugins/namespacescc/backup.go`:
- Line 99: Replace the uncancellable context used by the Namespaces().Get
lookups with a bounded-timeout context in both
velero-plugins/namespacescc/backup.go:99 and
velero-plugins/namespacescc/restore.go:104. Update the surrounding backup and
restore lookup flows to create and cancel the timeout context appropriately
while preserving the existing namespace retrieval behavior.
- Around line 59-60: Propagate all JSON conversion errors instead of ignoring
them, preventing partially populated ServiceAccount values or nil Objects from
being treated as successful. Update the conversion sites in
velero-plugins/namespacescc/backup.go at lines 59-60 and 73-75, and
velero-plugins/namespacescc/restore.go at lines 51-52, 56-58, and 78-80; either
return each Marshal/Unmarshal error through the surrounding backup and restore
flows or replace the round-trips with runtime.DefaultUnstructuredConverter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 69aa19f5-2bca-44fb-b8cc-45381b7806cd

📥 Commits

Reviewing files that changed from the base of the PR and between 2b9f8f8 and a4117e5.

📒 Files selected for processing (6)
  • velero-plugins/common/types.go
  • velero-plugins/main.go
  • velero-plugins/namespacescc/backup.go
  • velero-plugins/namespacescc/backup_test.go
  • velero-plugins/namespacescc/restore.go
  • velero-plugins/namespacescc/restore_test.go

Comment on lines +59 to +60
itemMarshal, _ := json.Marshal(item)
json.Unmarshal(itemMarshal, &serviceAccount)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -e
printf '=== outline backup.go ===\n'
ast-grep outline velero-plugins/namespacescc/backup.go --view expanded || true
printf '\n=== outline restore.go ===\n'
ast-grep outline velero-plugins/namespacescc/restore.go --view expanded || true
printf '\n=== backup.go excerpt ===\n'
cat -n velero-plugins/namespacescc/backup.go | sed -n '1,220p'
printf '\n=== restore.go excerpt ===\n'
cat -n velero-plugins/namespacescc/restore.go | sed -n '1,220p'

Repository: openshift/openshift-velero-plugin

Length of output: 14216


🏁 Script executed:

set -e
printf '=== json conversion usages in namespacescc ===\n'
rg -n 'json\.(Marshal|Unmarshal)|DefaultUnstructuredConverter|UnstructuredConverter' velero-plugins/namespacescc || true
printf '\n=== related tests or helpers ===\n'
rg -n 'namespacescc|ServiceAccount|unstructured' velero-plugins -g '*_test.go' || true

Repository: openshift/openshift-velero-plugin

Length of output: 1786


🏁 Script executed:

set -e
printf '=== backup_test.go excerpt ===\n'
cat -n velero-plugins/namespacescc/backup_test.go | sed -n '1,220p'
printf '\n=== restore_test.go excerpt ===\n'
cat -n velero-plugins/namespacescc/restore_test.go | sed -n '1,240p'

Repository: openshift/openshift-velero-plugin

Length of output: 8537


🏁 Script executed:

set -e
cat >/tmp/namespacescc_json_verify.go <<'GO'
package main

import (
	"encoding/json"
	"fmt"
)

type ServiceAccount struct {
	Namespace   string            `json:"namespace"`
	Annotations map[string]string `json:"annotations"`
}

func main() {
	bad := []byte(`{"namespace": 123, "annotations": {"a": "b"}}`)
	var sa ServiceAccount
	err := json.Unmarshal(bad, &sa)
	fmt.Printf("unmarshal err: %v\n", err)
	fmt.Printf("result: %#v\n", sa)

	var out map[string]interface{}
	marshalBad := []byte(`{"namespace":"ns","annotations":` + "`" + `oops` + "`" + `}`)
	err = json.Unmarshal(marshalBad, &out)
	fmt.Printf("unmarshal-to-map err: %v\n", err)
	fmt.Printf("map is nil: %v\n", out == nil)
}
GO
go run /tmp/namespacescc_json_verify.go

Repository: openshift/openshift-velero-plugin

Length of output: 451


Propagate these conversion errors.

These JSON round-trips are unchecked in both backup and restore. A failed decode can leave a partially populated ServiceAccount, and a failed encode/decode can return a nil Object while still reporting success. Return the error (or use runtime.DefaultUnstructuredConverter) at:

  • velero-plugins/namespacescc/backup.go#L59-L60, #L73-L75
  • velero-plugins/namespacescc/restore.go#L51-L52, #L56-L58, #L78-L80
📍 Affects 2 files
  • velero-plugins/namespacescc/backup.go#L59-L60 (this comment)
  • velero-plugins/namespacescc/backup.go#L73-L75
  • velero-plugins/namespacescc/restore.go#L51-L52
  • velero-plugins/namespacescc/restore.go#L56-L58
  • velero-plugins/namespacescc/restore.go#L78-L80
🤖 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 `@velero-plugins/namespacescc/backup.go` around lines 59 - 60, Propagate all
JSON conversion errors instead of ignoring them, preventing partially populated
ServiceAccount values or nil Objects from being treated as successful. Update
the conversion sites in velero-plugins/namespacescc/backup.go at lines 59-60 and
73-75, and velero-plugins/namespacescc/restore.go at lines 51-52, 56-58, and
78-80; either return each Marshal/Unmarshal error through the surrounding backup
and restore flows or replace the round-trips with
runtime.DefaultUnstructuredConverter.

Source: Path instructions

if err != nil {
return nil, err
}
ns, err := client.Namespaces().Get(context.Background(), namespace, metav1.GetOptions{})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf '\n## backup.go\n'
cat -n velero-plugins/namespacescc/backup.go | sed -n '1,220p'

printf '\n## restore.go\n'
cat -n velero-plugins/namespacescc/restore.go | sed -n '1,220p'

printf '\n## search for mutex/context usage\n'
rg -n "WithTimeout|context\\.Background|mutex|Lock\\(|Unlock\\(|ServiceAccount|Namespaces\\(\\)\\.Get" velero-plugins/namespacescc -S

Repository: openshift/openshift-velero-plugin

Length of output: 14037


Bound the namespace lookups with a timeout.
context.Background() leaves both Namespaces().Get(...) calls uncancellable, and each one runs while mu is held, so a slow apiserver can stall every ServiceAccount processed by the same plugin instance.

  • velero-plugins/namespacescc/backup.go#L99
  • velero-plugins/namespacescc/restore.go#L104
📍 Affects 2 files
  • velero-plugins/namespacescc/backup.go#L99-L99 (this comment)
  • velero-plugins/namespacescc/restore.go#L104-L104
🤖 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 `@velero-plugins/namespacescc/backup.go` at line 99, Replace the uncancellable
context used by the Namespaces().Get lookups with a bounded-timeout context in
both velero-plugins/namespacescc/backup.go:99 and
velero-plugins/namespacescc/restore.go:104. Update the surrounding backup and
restore lookup flows to create and cancel the timeout context appropriately
while preserving the existing namespace retrieval behavior.

Source: Path instructions

Namespace objects never pass through RestoreItemAction plugins in the
pinned Velero version (restore.go unconditionally skips them), so a
namespaces-scoped plugin as literally proposed in openshift#448 can never fire.

Instead, stash the namespace's SCC UID/GID-range annotations
(sa.scc.uid-range, sa.scc.supplemental-groups, sa.scc.mcs) onto each
backed-up ServiceAccount, and compare them against the actual restored
namespace's annotations at restore time, logging a warning on mismatch.

Fixes openshift#448

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com>
@kaovilai
kaovilai force-pushed the issue-448-scc-uid-gid-mismatch branch from a4117e5 to d597b27 Compare July 21, 2026 01:05
@openshift-ci openshift-ci Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 21, 2026
@openshift-ci

openshift-ci Bot commented Jul 21, 2026

Copy link
Copy Markdown

@kaovilai: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

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

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: detect/reconcile SCC UID/GID-range mismatch on Namespace restore

1 participant