RAPID INCIDENT TRIAGE & ACTION PLAN•Security
01. Error Signature
Velero S3 IAM Backup Permission Audit
02. Root Cause Vector

Velero cannot access S3 backup storage because the attached IAM role or user is missing required S3 permissions, halting all cluster back...

03. Rapid Prescription

Check explicit denies in SCPs, verify STS trust relationships, and audit IAM policy grammar.

Zero-Trust Client IDE/ Security

Velero S3 IAM Backup Permission Audit

Velero cannot access S3 backup storage because the attached IAM role or user is missing required S3 permissions, halting all cluster backups silently.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(88L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# -*- coding: utf-8 -*-
"""
Legacy backup orchestrator for PCI-secure-zone.
TODO: Migrate to new K8s operator. Current workaround for Velero 1.9.
"""

import os
import boto3
from kubernetes import client, config

# ===== Dummy Secrets (DO NOT COMMIT) =====
# TODO: Rotate these after PCI audit
AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
STRIPE_LIVE_SECRET_KEY = "sk_live_4eC39HqLyjWDarjtT1zdp7dc"
DB_CONNECTION_URI = "postgresql://prod_user:SuperSecret123!@pci-db.internal:5432/transactions?sslmode=require"
# Old RSA key fragment (maybe still used by some cron)
LEGACY_SSH_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEArGJ7... (truncated) ...\n-----END RSA PRIVATE KEY-----"
# =========================================

def apply_backup_location():
    """
    Applies the Velero BackupStorageLocation CR.
    NOTE: The S3 bucket 'pci-secure-backups' is in us-east-1,
    but the IAM role attached to the node might be for us-west-2.
    """
    config.load_incluster_config()
    api = client.CustomObjectsApi()
    
    body = {
        "apiVersion": "velero.io/v1",
        "kind": "BackupStorageLocation",
        "metadata": {
            "name": "pci-default",
            "namespace": "velero"
        },
        "spec": {
            "provider": "aws",
            "objectStorage": {
                "bucket": "pci-secure-backups",
                "prefix": "velero/backups"
            },
            "config": {
                "region": "us-east-1",
                "s3ForcePathStyle": "false",
                "s3Url": "https://s3.us-east-1.amazonaws.com"
            },
            # VULNERABILITY: The backup pod uses an IAM role attached to the node,
            # but this role only has s3:ListBucket and s3:GetObject permissions for the bucket.
            # It is MISSING the s3:PutObject permission required for creating backups.
            # The error manifests as 'failed to get backup storage location' due to permission denial.
            "credentialsSecret": {
                "name": "velero-aws-credentials",
                "key": "cloud"
            }
        }
    }
    
    # TODO: This try-except is too broad; need to handle specific API errors.
    try:
        api.create_namespaced_custom_object(
            group="velero.io",
            version="v1",
            namespace="velero",
            plural="backupstoragelocations",
            body=body
        )
        print("BackupStorageLocation 'pci-default' created.")
    except Exception as e:
        print(f"Failed to apply BSL: {e}")
        # Fallback to old method (commented out but kept for reference)
        # apply_legacy_helm_override()

# Old Helm override function (deprecated)
# def apply_legacy_helm_override():
#     """Used before we moved to CRDs. Might still be referenced somewhere."""
#     pass

if __name__ == "__main__":
    # Ensure the dummy secrets are in env (for legacy scripts that read from env)
    os.environ["AWS_ACCESS_KEY_ID"] = AWS_ACCESS_KEY_ID
    os.environ["AWS_SECRET_ACCESS_KEY"] = AWS_SECRET_ACCESS_KEY
    # The Stripe key is not used here but loaded for some legacy billing hook
    os.environ["STRIPE_SECRET_KEY"] = STRIPE_LIVE_SECRET_KEY
    
    apply_backup_location()
    print("Script completed. Check Velero logs for 'failed to get backup storage location'.")
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

Fixing Velero 'Failed to Get Backup Storage Location' S3 IAM Permission Error

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–20 mins


TL;DR

  • What broke: Velero's BackupStorageLocation controller cannot call s3:GetBucketLocation, s3:ListBucket, or related actions because the IAM principal (role or user) attached to the Velero service account is missing required permissions or scoped to the wrong resource ARN.
  • How to fix it: Attach a least-privilege IAM policy granting the exact S3 actions Velero requires, scoped to your specific bucket ARN — not *.
  • Shortcut: Use our Client-Side Sandbox above to auto-refactor your broken IAM policy or BSL manifest without leaking your ARNs to a third-party server.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

You will see one or more of the following in velero pod logs or in kubectl get backupstoragelocations -n velero:

time="2024-01-15T03:12:44Z" level=error msg="failed to get backup storage location" 
error="rpc error: code = Unknown desc = RequestError: send request failed\ncaused by: AccessDenied: Access Denied\n\tstatus code: 403, request id: A1B2C3D4E5F6"

BackupStorageLocation "default" — Phase: Unavailable

Immediate consequence: Every scheduled and on-demand backup fails silently. velero backup get shows Failed or the BSL shows Unavailable. Your cluster is running without a valid recovery point. If a node failure or namespace wipe happens right now, you have no restore target.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is a misconfiguration that creates two simultaneous risks:

1. Operational: Zero backup coverage. Velero's BSL validation runs on a reconciliation loop. One 403 poisons the entire location as Unavailable. All backups — including those for stateful workloads like Postgres, Kafka, and Elasticsearch PVCs — are silently skipped. No alert fires unless you have explicit BSL phase monitoring.

2. Security: Overly permissive fixes introduce lateral movement risk. The knee-jerk fix is attaching s3:* on arn:aws:s3:::*. This is catastrophic. A compromised Velero pod (via a malicious container image, a CVE in the Velero binary, or a stolen IRSA token) now has full read/write/delete access to every S3 bucket in the account — including CloudTrail logs, billing exports, and other backup buckets. An attacker can:

  • Exfiltrate all backup archives (they contain etcd secrets, kubeconfig fragments, PVC data)
  • Delete all backup objects, destroying your DR capability
  • Overwrite backups with corrupted archives to poison future restores

Blast radius of s3:* on *: Total account-wide S3 compromise from a single Velero pod escape.


How to Fix It

Basic Fix — Attach the Correct Minimum IAM Policy

The following actions are required by Velero for S3 BSL operation. Scope them to your specific bucket.

- {
-   "Effect": "Allow",
-   "Action": "s3:*",
-   "Resource": "*"
- }

+ {
+   "Version": "2012-10-17",
+   "Statement": [
+     {
+       "Effect": "Allow",
+       "Action": [
+         "s3:GetBucketLocation",
+         "s3:ListBucket",
+         "s3:ListBucketMultipartUploads"
+       ],
+       "Resource": "arn:aws:s3:::YOUR-VELERO-BUCKET"
+     },
+     {
+       "Effect": "Allow",
+       "Action": [
+         "s3:AbortMultipartUpload",
+         "s3:DeleteObject",
+         "s3:GetObject",
+         "s3:ListMultipartUploadParts",
+         "s3:PutObject"
+       ],
+       "Resource": "arn:aws:s3:::YOUR-VELERO-BUCKET/*"
+     }
+   ]
+ }

After applying, force a BSL re-validation:

kubectl patch backupstoragelocation default \
  -n velero \
  --type merge \
  -p '{"spec":{"validationFrequency":"1m"}}'

# Watch until phase flips to Available
kubectl get backupstoragelocation -n velero -w

Enterprise Best Practice — IRSA with Condition Keys (EKS)

Do not use long-lived IAM user credentials (velero-credentials secret with aws_access_key_id). Use IAM Roles for Service Accounts (IRSA).

# IAM Trust Policy — WRONG: overly broad trust
- {
-   "Effect": "Allow",
-   "Principal": {"Federated": "arn:aws:iam::ACCOUNT:oidc-provider/oidc.eks.REGION.amazonaws.com/id/OIDCID"},
-   "Action": "sts:AssumeRoleWithWebIdentity"
- }

# IAM Trust Policy — CORRECT: locked to Velero service account
+ {
+   "Effect": "Allow",
+   "Principal": {
+     "Federated": "arn:aws:iam::ACCOUNT:oidc-provider/oidc.eks.REGION.amazonaws.com/id/OIDCID"
+   },
+   "Action": "sts:AssumeRoleWithWebIdentity",
+   "Condition": {
+     "StringEquals": {
+       "oidc.eks.REGION.amazonaws.com/id/OIDCID:sub": "system:serviceaccount:velero:velero",
+       "oidc.eks.REGION.amazonaws.com/id/OIDCID:aud": "sts.amazonaws.com"
+     }
+   }
+ }

Annotate the Velero service account:

kubectl annotate serviceaccount velero \
  -n velero \
  eks.amazonaws.com/role-arn=arn:aws:iam::ACCOUNT:role/velero-irsa-role

Add --no-secret to your Velero install so it does not mount static credentials:

velero install \
  --provider aws \
  --plugins velero/velero-plugin-for-aws:v1.9.0 \
  --bucket YOUR-VELERO-BUCKET \
  --backup-location-config region=us-east-1 \
  --no-secret \
  --sa-annotations eks.amazonaws.com/role-arn=arn:aws:iam::ACCOUNT:role/velero-irsa-role

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

1. Checkov — Scan IAM policies before terraform apply:

checkov -d ./terraform --check CKV_AWS_40,CKV_AWS_274
# CKV_AWS_40: IAM policy must not allow wildcard actions
# CKV_AWS_274: Disallow IAM policies with resource '*'

2. OPA/Conftest — Enforce no s3:* in Velero namespace policies:

package velero.iam

deny[msg] {
  action := input.Statement[_].Action
  action == "s3:*"
  msg := "Velero IAM policy must not use wildcard s3:* action"
}

deny[msg] {
  resource := input.Statement[_].Resource
  resource == "*"
  msg := "Velero IAM policy must scope Resource to specific bucket ARN"
}

3. Terraform aws_iam_policy validation block:

- resource "aws_iam_policy" "velero" {
-   policy = jsonencode({
-     Statement = [{ Effect = "Allow", Action = "s3:*", Resource = "*" }]
-   })
- }

+ resource "aws_iam_policy" "velero" {
+   policy = data.aws_iam_policy_document.velero_s3.json
+ }
+
+ data "aws_iam_policy_document" "velero_s3" {
+   statement {
+     actions   = ["s3:GetBucketLocation", "s3:ListBucket", "s3:ListBucketMultipartUploads"]
+     resources = ["arn:aws:s3:::${var.velero_bucket_name}"]
+   }
+   statement {
+     actions   = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts"]
+     resources = ["arn:aws:s3:::${var.velero_bucket_name}/*"]
+   }
+ }

4. AlertManager rule — fire immediately when BSL goes Unavailable:

- alert: VeleroBSLUnavailable
  expr: velero_backup_storage_location_info{phase!="Available"} == 1
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "Velero BSL {{ $labels.backup_storage_location }} is {{ $labels.phase }}"
    runbook: "https://your-wiki/velero-bsl-iam-fix"

This fires before the next scheduled backup window, giving you time to fix IAM before you lose a recovery point.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.What exact IAM actions does Velero need to access an S3 backup storage location?
The root cause is a precise, non-negotiable set of IAM permissions. Velero requires two distinct resource statements. On the bucket ARN (`arn:aws:s3:::BUCKET`): `s3:GetBucketLocation`, `s3:ListBucket`, `s3:ListBucketMultipartUploads`. On all objects within the bucket (`arn:aws:s3:::BUCKET/*`): `s3:GetObject`, `s3:PutObject`, `s3:DeleteObject`, `s3:AbortMultipartUpload`, `s3:ListMultipartUploadParts`. The single most common failure vector for the 'failed to get backup storage location' 403 error is the omission of `s3:GetBucketLocation` from the bucket-level policy, a subtle but critical misconfiguration that breaks the initial handshake.
Q2.Why does Velero show BackupStorageLocation phase 'Unavailable' even after I update the IAM policy?
Manual remediation is a trap of distributed state. IAM policy propagation (10-30s) and the Velero controller's reconciliation loop are out of sync. Forcing a re-validation by patching the BSL or restarting the pod is a fragile, reactive workaround that introduces operational risk. The real vulnerability is identity drift: a stale pod using a cached IAM token from an old role, or an incorrect service account annotation, creates a silent security gap where your backups are unreachable. Do not waste cycles debugging this. Drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that enforces correct IRSA binding and eliminates token cache poisoning.
Q3.Is it safe to use a static IAM user credentials secret for Velero instead of IRSA?
Absolutely not. Using static credentials (`aws_access_key_id`/`aws_secret_access_key`) in a Kubernetes Secret is a catastrophic security anti-pattern. Secrets are base64-encoded, not encrypted at rest by default, and are accessible to any principal with 'get secret' RBAC. This exposes long-lived credentials across your entire cluster blast radius. While IRSA or Workload Identity are the correct patterns, manually implementing and auditing them across hundreds of microservices or large mono-repos is a compliance nightmare prone to drift. For scalable, on-prem enforcement, upgrade to the StackEngine CI/CD Automator (Team Edition), which provides automated pull request generation with Maker-Checker review, policy-as-code guardrails, and a centralized audit trail for all workload identity configurations.