setup

Kubernetes

Purpose

Discovers and analyzes Kubernetes resources inside EKS and self-managed clusters — nodes, pods, services, ingresses, Gateway API routes, RBAC, namespaces, and secret metadata. EKS clusters also include EKS-specific identity mappings and external OIDC provider configuration.

Prerequisites

  • For EKS rows, the aws module is configured for the account containing your EKS cluster. EKS reuses the AWS IAM role to authenticate and build each cluster's kubeconfig.
  • For EKS rows, you have permission to grant the SubImageScanRole IAM role access to the cluster (either via EKS Access Entries or the aws-auth ConfigMap).
  • For self-managed rows, the SubImage outpost Helm chart is installed in the cluster and the module row points at that outpost hostname. Self-managed clusters use the outpost ServiceAccount token; raw kubeconfig upload is not supported.

Add Clusters

In SubImage, go to Modules → Kubernetes and add one row per cluster.

  • Choose EKS for AWS-managed clusters. Enter the cluster ARN in the format arn:aws:eks:<region>:<account-id>:cluster/<cluster-name>. SubImage parses the account ID and region from the ARN.
  • For EKS clusters with private API endpoints, enter an outpost hostname on that row. The row-level hostname takes precedence over the module-level Tailscale outpost hostname.
  • Choose Self-managed for on-premise or self-managed clusters reachable through a SubImage outpost. Enter a stable cluster name and the outpost hostname for that cluster.

Save the module and trigger a sync.

EKS AWS IAM Permissions

For EKS rows, SubImage uses the AWS scan role for two things:

  1. Build the kubeconfig via eks:DescribeCluster.
  2. Enrich EKS identity metadata via eks:ListAccessEntries, eks:DescribeAccessEntry, eks:ListIdentityProviderConfigs, eks:DescribeIdentityProviderConfig.

If you deployed SubImageScanRole from the current AWS setup template, no changes are required — those permissions are included. If the role was deployed from an older StackSet or standalone stack, update that deployment from the current AWS setup template; editing the module row in SubImage does not update customer IAM. SecurityAudit already covers eks:DescribeCluster and eks:ListAccessEntries.

Only if you're extending an older scan role (that only has SecurityAudit attached), add this inline policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "eks:DescribeAccessEntry",
        "eks:ListIdentityProviderConfigs",
        "eks:DescribeIdentityProviderConfig"
      ],
      "Resource": "*"
    }
  ]
}

EKS Cluster Access

Pick the method that matches your cluster's authentication mode.

This is the preferred method for newer clusters.

  1. Enable API access mode, if not already enabled:

    aws eks update-cluster-config \
      --name <cluster-name> \
      --access-config authenticationMode=API_AND_CONFIG_MAP
  2. Create an access entry for SubImageScanRole:

    aws eks create-access-entry \
      --cluster-name <cluster-name> \
      --principal-arn "arn:aws:iam::<YOUR_ACCOUNT_ID>:role/SubImageScanRole" \
      --type STANDARD \
      --username subimage-scan
  3. Create the read-only ClusterRole:

    kubectl apply -f - <<EOF
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: subimage-viewer
    rules:
    # Namespaces — list for namespace sync, get for kube-system cluster metadata
    - apiGroups: [""]
      resources:
        - namespaces
      verbs: ["get", "list"]
    # Core resources
    - apiGroups: [""]
      resources:
        - nodes
        - pods
        - services
        - serviceaccounts
      verbs: ["list"]
    # Secrets — Cartography stores metadata only, but list still returns payloads from the API server
    - apiGroups: [""]
      resources:
        - secrets
      verbs: ["list"]
    - apiGroups: ["rbac.authorization.k8s.io"]
      resources:
        - roles
        - rolebindings
        - clusterroles
        - clusterrolebindings
      verbs: ["list"]
    # Workload controllers — enable the WORKLOAD_PARENT chain from a pod up to its owning controller
    - apiGroups: ["apps"]
      resources:
        - deployments
        - replicasets
        - statefulsets
        - daemonsets
      verbs: ["list"]
    - apiGroups: ["batch"]
      resources:
        - jobs
        - cronjobs
      verbs: ["list"]
    - apiGroups: ["networking.k8s.io"]
      resources:
        - ingresses
        - networkpolicies
      verbs: ["list"]
    # Gateway API resources — optional, only useful when the Gateway API CRDs are installed
    - apiGroups: ["gateway.networking.k8s.io"]
      resources:
        - gateways
        - httproutes
      verbs: ["list"]
    # aws-auth identity mapping
    - apiGroups: [""]
      resources:
        - configmaps
      verbs: ["get"]
    EOF
  4. Bind it:

    kubectl create clusterrolebinding subimage-scan-viewer \
      --clusterrole=subimage-viewer \
      --user=subimage-scan

Terraform equivalent:

resource "aws_eks_access_entry" "subimage_scan" {
  cluster_name  = aws_eks_cluster.your_cluster.name
  principal_arn = "arn:aws:iam::<YOUR_ACCOUNT_ID>:role/SubImageScanRole"
  type          = "STANDARD"
  user_name     = "subimage-scan"
}

resource "kubernetes_cluster_role" "subimage_viewer" {
  metadata { name = "subimage-viewer" }

  rule {
    api_groups = [""]
    resources  = ["namespaces"]
    verbs      = ["get", "list"]
  }
  rule {
    api_groups = [""]
    resources  = ["nodes", "pods", "services", "serviceaccounts"]
    verbs      = ["list"]
  }
  rule {
    api_groups = [""]
    resources  = ["secrets"]
    verbs      = ["list"]
  }
  rule {
    api_groups = ["rbac.authorization.k8s.io"]
    resources  = ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
    verbs      = ["list"]
  }
  rule {
    api_groups = ["apps"]
    resources  = ["deployments", "replicasets", "statefulsets", "daemonsets"]
    verbs      = ["list"]
  }
  rule {
    api_groups = ["batch"]
    resources  = ["jobs", "cronjobs"]
    verbs      = ["list"]
  }
  rule {
    api_groups = ["networking.k8s.io"]
    resources  = ["ingresses", "networkpolicies"]
    verbs      = ["list"]
  }
  rule {
    api_groups = ["gateway.networking.k8s.io"]
    resources  = ["gateways", "httproutes"]
    verbs      = ["list"]
  }
  rule {
    api_groups = [""]
    resources  = ["configmaps"]
    verbs      = ["get"]
  }
}

resource "kubernetes_cluster_role_binding" "subimage_scan_viewer" {
  metadata { name = "subimage-scan-viewer" }

  role_ref {
    api_group = "rbac.authorization.k8s.io"
    kind      = "ClusterRole"
    name      = kubernetes_cluster_role.subimage_viewer.metadata[0].name
  }

  subject {
    kind      = "User"
    name      = "subimage-scan"
    api_group = "rbac.authorization.k8s.io"
  }
}

Method 2 — aws-auth ConfigMap (traditional)

For clusters still on the ConfigMap-based access model.

  1. Edit the aws-auth ConfigMap in kube-system:

    kubectl edit configmap aws-auth -n kube-system
  2. Add the scan role to mapRoles:

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: aws-auth
      namespace: kube-system
    data:
      mapRoles: |
        # ... existing mappings ...
        - rolearn: arn:aws:iam::<YOUR_ACCOUNT_ID>:role/SubImageScanRole
          username: subimage-scan
  3. Apply the same ClusterRole and ClusterRoleBinding shown in Method 1.

Terraform equivalent for the ConfigMap part:

resource "kubernetes_config_map" "aws_auth" {
  metadata {
    name      = "aws-auth"
    namespace = "kube-system"
  }

  data = {
    mapRoles = yamlencode([
      {
        rolearn  = "arn:aws:iam::<YOUR_ACCOUNT_ID>:role/SubImageScanRole"
        username = "subimage-scan"
      }
    ])
  }
}

Self-Managed Cluster Access

Self-managed rows do not use AWS IAM auth, EKS access entries, or the aws-auth ConfigMap. SubImage connects through the outpost and uses the outpost pod's Kubernetes ServiceAccount token.

  1. Install the SubImage outpost Helm chart inside the cluster you want to scan.
  2. Leave the outpost proxyTarget set to https://kubernetes.default.svc when the outpost is scanning the same cluster where it runs.
  3. Confirm the outpost ServiceAccount has read-only RBAC for Kubernetes resources. The Helm chart includes the expected RBAC; use the chart README for advanced customization.
  4. In Modules → Kubernetes, add a Self-managed row with:
    • Cluster name: any stable name that identifies this cluster in SubImage, for example production-us-east.
    • Outpost: the Tailscale hostname for the outpost, for example <TENANT_ID>-cluster-name-outpost.

Troubleshooting

  • User "system:anonymous" cannot get resource... or Kubernetes 401 Unauthorized — authentication problem. For EKS rows, verify the cluster's auth mode (aws eks describe-cluster --name <cluster-name> --query 'cluster.accessConfig'), the role ARN in your config, and that the access entry (or aws-auth row) references the same username as the ClusterRoleBinding (subimage-scan). For self-managed rows, verify the outpost is proxying to the Kubernetes API and the outpost ServiceAccount token is mounted.
  • The config profile (tenant-account) could not be found — for EKS rows, the AWS account ID in the cluster ARN does not match any account in the AWS integration. Ensure the AWS module has synced successfully and includes the account.
  • Kubernetes 403 Forbidden — SubImage authenticated but RBAC blocks the request. Verify the ClusterRoleBinding exists and the subimage-viewer ClusterRole covers all resources listed above.
  • Outpost sync reaches 404 Not Found — the outpost is likely proxying to something other than the Kubernetes API server. For in-cluster Helm deployments, use https://kubernetes.default.svc as the proxy target.
  • Sync reports "Cluster API Server Unreachable": the outpost is running and reachable, but connecting to the Kubernetes API server timed out or was refused, so it returned its own error instead of the cluster's response. Check that no NetworkPolicy or egress rule blocks the outpost pod from reaching the API server, that cluster DNS resolves kubernetes.default.svc, and that the API server is healthy and not overloaded. If the cluster is simply slow, raise PROXY_CONNECT_TIMEOUT or PROXY_READ_TIMEOUT on the outpost. The outpost log, available from the outposts settings page, records the underlying error.
  • Sync reports "Outpost Proxy Error": the outpost returned an error of its own rather than the cluster's response. Verify that the proxy target is a valid Kubernetes API URL (https://kubernetes.default.svc for in-cluster Helm installs). On outposts older than 1.2.0 this error cannot distinguish an unreachable API server from a failure inside the outpost; upgrade to outpost 1.2.0 or later (chart 1.4.0), which reports the cause.
  • AWS AccessDenied before the request reaches KubernetesSubImageScanRole is missing one of the EKS IAM permissions listed at the top. This is separate from Kubernetes RBAC.

Only read access is granted or required. SubImage stores secret metadata only (name, type) — but note that Kubernetes list on secrets can still return payloads from the API server.