|
| 1 | +# AWS command-line tool |
| 2 | + |
| 3 | +The AWS command-line tool allows KubeAI to run `aws` commands against AWS resources on behalf of the user. It follows the same execution model as the kubectl tool — commands are parsed and executed directly without a shell, preventing injection attacks. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## What the AWS Tool Can Do |
| 8 | + |
| 9 | +When enabled, the LLM can run `aws` commands to: |
| 10 | + |
| 11 | + - Describe and list EC2 instances, security groups, VPCs, subnets |
| 12 | + - Query EKS clusters (`aws eks describe-cluster`, `aws eks list-clusters`) |
| 13 | + - Inspect load balancers (ALB/NLB via `aws elbv2`) |
| 14 | + - Check IAM roles and policies (`aws iam get-role`, `aws iam list-attached-role-policies`) |
| 15 | + - Query CloudWatch metrics and log groups |
| 16 | + - List S3 buckets and objects |
| 17 | + - Query RDS instances and snapshots |
| 18 | + - Inspect Route53 hosted zones and records |
| 19 | + - Run `aws sts get-caller-identity` to verify credentials |
| 20 | + |
| 21 | +### Commands that are always blocked |
| 22 | + |
| 23 | +Regardless of `ENABLE_AWS_TOOL`, the following are rejected at the validation layer: |
| 24 | + |
| 25 | +| Blocked command | Reason | |
| 26 | +| -------------------------------------- | ---------------------------- | |
| 27 | +| `aws secretsmanager get-secret-value` | Secret retrieval | |
| 28 | +| `aws ssm get-parameter` | Secret retrieval | |
| 29 | +| `aws ssm get-parameters` | Secret retrieval | |
| 30 | +| `aws ssm get-parameters-by-path` | Secret retrieval | |
| 31 | +| `aws kms decrypt` | Credential/secret decryption | |
| 32 | +| `aws kms generate-data-key` | Credential/secret decryption | |
| 33 | +| `aws iam create-access-key` | Credential creation | |
| 34 | +| `aws sts assume-role` | Credential escalation | |
| 35 | +| Any compound command (`\|`, `&&`, `;`) | Shell injection prevention | |
| 36 | +| `aws configure` / `aws sso login` | Interactive mode | |
| 37 | + |
| 38 | +--- |
| 39 | + |
| 40 | +## Enabling the AWS Tool |
| 41 | + |
| 42 | +The tool is **disabled by default**. Set `ENABLE_AWS_TOOL=true` in the pod's environment: |
| 43 | + |
| 44 | +```yaml |
| 45 | +# values.yaml |
| 46 | +env: |
| 47 | + ENABLE_AWS_TOOL: "true" |
| 48 | +``` |
| 49 | +
|
| 50 | +No other configuration is required when the pod already has IRSA credentials — the tool inherits them from the process environment automatically. |
| 51 | +
|
| 52 | +--- |
| 53 | +
|
| 54 | +## IAM Permissions |
| 55 | +
|
| 56 | +The IRSA role attached to the KubeAI service account must have the permissions you want the LLM to use. A minimal read-only policy example: |
| 57 | +
|
| 58 | +```json |
| 59 | +{ |
| 60 | + "Version": "2012-10-17", |
| 61 | + "Statement": [ |
| 62 | + { |
| 63 | + "Effect": "Allow", |
| 64 | + "Action": [ |
| 65 | + "ec2:Describe*", |
| 66 | + "eks:DescribeCluster", |
| 67 | + "eks:ListClusters", |
| 68 | + "elasticloadbalancing:Describe*", |
| 69 | + "iam:GetRole", |
| 70 | + "iam:ListAttachedRolePolicies", |
| 71 | + "iam:ListRolePolicies", |
| 72 | + "iam:GetRolePolicy", |
| 73 | + "cloudwatch:GetMetricStatistics", |
| 74 | + "cloudwatch:ListMetrics", |
| 75 | + "logs:DescribeLogGroups", |
| 76 | + "logs:DescribeLogStreams", |
| 77 | + "s3:ListAllMyBuckets", |
| 78 | + "s3:ListBucket", |
| 79 | + "rds:DescribeDBInstances", |
| 80 | + "route53:ListHostedZones", |
| 81 | + "route53:ListResourceRecordSets", |
| 82 | + "sts:GetCallerIdentity" |
| 83 | + ], |
| 84 | + "Resource": "*" |
| 85 | + } |
| 86 | + ] |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +Attach this policy to the IRSA role used by the KubeAI service account. Follow the IRSA setup in [cross_cluster_access.md](cross_cluster_access.md) for the full role and trust policy setup. |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +## Cross-Account AWS Access |
| 95 | + |
| 96 | +`aws sts assume-role` is blocked by the tool's validation layer, so cross-account access must be configured at the infrastructure level. There are two approaches depending on how many accounts you need. |
| 97 | + |
| 98 | +--- |
| 99 | + |
| 100 | +### Option A — IRSA only (direct OIDC trust, no config file needed) |
| 101 | + |
| 102 | +Account B registers Account A's OIDC provider directly in its own IAM. The pod then assumes Account B's role in a **single** `sts:AssumeRoleWithWebIdentity` call — no `~/.aws/config`, no role chaining. This is the approach described in the [AWS cross-account IRSA guide](https://docs.aws.amazon.com/eks/latest/userguide/cross-account-access.html). |
| 103 | + |
| 104 | +Use this when KubeAI only needs to access Account B resources (the service account annotation points to Account B's role, so all commands run in Account B's context). |
| 105 | + |
| 106 | +#### Step 1: Get Account A's OIDC issuer URL |
| 107 | + |
| 108 | +Run this in **Account A**: |
| 109 | + |
| 110 | +```bash |
| 111 | +OIDC_ISSUER=$(aws eks describe-cluster \ |
| 112 | + --name cluster-a --region ap-southeast-1 \ |
| 113 | + --query "cluster.identity.oidc.issuer" --output text) |
| 114 | +# e.g. https://oidc.eks.ap-southeast-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E |
| 115 | +``` |
| 116 | + |
| 117 | +#### Step 2: Register Account A's OIDC provider in Account B |
| 118 | + |
| 119 | +Run this in **Account B**: |
| 120 | + |
| 121 | +```bash |
| 122 | +# Get the OIDC thumbprint |
| 123 | +THUMBPRINT=$(openssl s_client -connect oidc.eks.ap-southeast-1.amazonaws.com:443 \ |
| 124 | + -servername oidc.eks.ap-southeast-1.amazonaws.com 2>/dev/null \ |
| 125 | + | openssl x509 -fingerprint -noout \ |
| 126 | + | sed 's/SHA1 Fingerprint=//' | tr -d ':' | tr '[:upper:]' '[:lower:]') |
| 127 | + |
| 128 | +aws iam create-open-id-connect-provider \ |
| 129 | + --url $OIDC_ISSUER \ |
| 130 | + --client-id-list sts.amazonaws.com \ |
| 131 | + --thumbprint-list $THUMBPRINT |
| 132 | +# Output: arn:aws:iam::ACCOUNT_B_ID:oidc-provider/oidc.eks.ap-southeast-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E |
| 133 | +``` |
| 134 | + |
| 135 | +#### Step 3: Create a role in Account B that trusts Account A's OIDC provider |
| 136 | + |
| 137 | +Run this in **Account B**, using the OIDC issuer path (without `https://`): |
| 138 | + |
| 139 | +```bash |
| 140 | +OIDC_PROVIDER="oidc.eks.ap-southeast-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E" |
| 141 | + |
| 142 | +cat > trust-policy.json <<EOF |
| 143 | +{ |
| 144 | + "Version": "2012-10-17", |
| 145 | + "Statement": [ |
| 146 | + { |
| 147 | + "Effect": "Allow", |
| 148 | + "Principal": { |
| 149 | + "Federated": "arn:aws:iam::ACCOUNT_B_ID:oidc-provider/${OIDC_PROVIDER}" |
| 150 | + }, |
| 151 | + "Action": "sts:AssumeRoleWithWebIdentity", |
| 152 | + "Condition": { |
| 153 | + "StringEquals": { |
| 154 | + "${OIDC_PROVIDER}:sub": "system:serviceaccount:kubeai-chatbot:kubeai-chatbot", |
| 155 | + "${OIDC_PROVIDER}:aud": "sts.amazonaws.com" |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | + ] |
| 160 | +} |
| 161 | +EOF |
| 162 | + |
| 163 | +aws iam create-role \ |
| 164 | + --role-name kubeai-chatbot-cross-account \ |
| 165 | + --assume-role-policy-document file://trust-policy.json |
| 166 | + |
| 167 | +aws iam attach-role-policy \ |
| 168 | + --role-name kubeai-chatbot-cross-account \ |
| 169 | + --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess |
| 170 | +``` |
| 171 | + |
| 172 | +#### Step 4: Annotate the KubeAI service account with the Account B role |
| 173 | + |
| 174 | +```yaml |
| 175 | +# values.yaml |
| 176 | +serviceAccount: |
| 177 | + annotations: |
| 178 | + eks.amazonaws.com/role-arn: "arn:aws:iam::ACCOUNT_B_ID:role/kubeai-chatbot-cross-account" |
| 179 | +``` |
| 180 | +
|
| 181 | +All `aws` commands now run as the Account B role. No `~/.aws/config` needed. |
| 182 | + |
| 183 | +--- |
| 184 | + |
| 185 | +### Option B — IRSA + aws config profiles (multiple accounts) |
| 186 | + |
| 187 | +Use this when KubeAI needs access to **both** Account A and Account B. The IRSA role stays in Account A (default credentials). A named profile in `~/.aws/config` tells the AWS CLI how to assume the Account B role on demand — the SDK handles the `sts:AssumeRole` call transparently when `--profile account-b` is used. |
| 188 | + |
| 189 | +#### Step 1: Create a cross-account role in Account B |
| 190 | + |
| 191 | +```bash |
| 192 | +cat > cross-account-trust.json <<EOF |
| 193 | +{ |
| 194 | + "Version": "2012-10-17", |
| 195 | + "Statement": [ |
| 196 | + { |
| 197 | + "Effect": "Allow", |
| 198 | + "Principal": { |
| 199 | + "AWS": "arn:aws:iam::ACCOUNT_A_ID:role/kubeai-chatbot" |
| 200 | + }, |
| 201 | + "Action": "sts:AssumeRole" |
| 202 | + } |
| 203 | + ] |
| 204 | +} |
| 205 | +EOF |
| 206 | +
|
| 207 | +aws iam create-role \ |
| 208 | + --role-name KubeAICrossAccountRole \ |
| 209 | + --assume-role-policy-document file://cross-account-trust.json |
| 210 | +
|
| 211 | +aws iam attach-role-policy \ |
| 212 | + --role-name KubeAICrossAccountRole \ |
| 213 | + --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess |
| 214 | +``` |
| 215 | + |
| 216 | +#### Step 2: Allow the Account A IRSA role to assume the Account B role |
| 217 | + |
| 218 | +Add `sts:AssumeRole` to the IRSA role policy in Account A: |
| 219 | + |
| 220 | +```json |
| 221 | +{ |
| 222 | + "Effect": "Allow", |
| 223 | + "Action": "sts:AssumeRole", |
| 224 | + "Resource": "arn:aws:iam::ACCOUNT_B_ID:role/KubeAICrossAccountRole" |
| 225 | +} |
| 226 | +``` |
| 227 | + |
| 228 | +#### Step 3: Mount `~/.aws/config` via ConfigMap |
| 229 | + |
| 230 | +```yaml |
| 231 | +apiVersion: v1 |
| 232 | +kind: ConfigMap |
| 233 | +metadata: |
| 234 | + name: aws-config |
| 235 | + namespace: kubeai-chatbot |
| 236 | +data: |
| 237 | + config: | |
| 238 | + [default] |
| 239 | + region = ap-southeast-1 |
| 240 | +
|
| 241 | + [profile account-b] |
| 242 | + role_arn = arn:aws:iam::ACCOUNT_B_ID:role/KubeAICrossAccountRole |
| 243 | + credential_source = EcsContainer |
| 244 | +``` |
| 245 | + |
| 246 | +```yaml |
| 247 | +# values.yaml |
| 248 | +volumes: |
| 249 | + - name: aws-config |
| 250 | + configMap: |
| 251 | + name: aws-config |
| 252 | +
|
| 253 | +volumeMounts: |
| 254 | + - name: aws-config |
| 255 | + mountPath: /home/kubeai/.aws |
| 256 | + readOnly: true |
| 257 | +``` |
| 258 | + |
| 259 | +> `credential_source = EcsContainer` works on EKS because IRSA is compatible with the ECS container credential provider. Alternatively use `credential_source = Environment`. |
| 260 | + |
| 261 | +Users instruct KubeAI to use Account B by specifying the profile in their message: |
| 262 | + |
| 263 | +```bash |
| 264 | +Show me all EC2 instances in Account B using profile account-b |
| 265 | +``` |
| 266 | + |
| 267 | +The LLM appends `--profile account-b` to commands, e.g.: |
| 268 | + |
| 269 | +```bash |
| 270 | +aws ec2 describe-instances --region ap-southeast-1 --profile account-b |
| 271 | +``` |
| 272 | + |
| 273 | +--- |
| 274 | + |
| 275 | +## Verify the Setup |
| 276 | + |
| 277 | +Exec into the KubeAI pod and confirm the AWS CLI works: |
| 278 | + |
| 279 | +```bash |
| 280 | +kubectl exec -it deployment/kubeai-chatbot -n kubeai-chatbot -- /bin/bash |
| 281 | +
|
| 282 | +# Confirm IRSA credentials are active |
| 283 | +aws sts get-caller-identity |
| 284 | +
|
| 285 | +# Test a read query |
| 286 | +aws eks list-clusters --region ap-southeast-1 |
| 287 | +
|
| 288 | +# Test cross-account (if configured) |
| 289 | +aws sts get-caller-identity --profile account-b |
| 290 | +``` |
| 291 | + |
| 292 | +--- |
| 293 | + |
| 294 | +## Troubleshooting |
| 295 | + |
| 296 | +**`ENABLE_AWS_TOOL` is set but the LLM does not use AWS commands** |
| 297 | + |
| 298 | + - Confirm the env var value is exactly `"true"` (string, not boolean). |
| 299 | + - Restart the pod after changing env vars. |
| 300 | + |
| 301 | +**`NoCredentialProviders` error** |
| 302 | + |
| 303 | + - IRSA is not active. Check the service account annotation: `eks.amazonaws.com/role-arn`. |
| 304 | + - Verify the IRSA token is mounted: `ls /var/run/secrets/eks.amazonaws.com/serviceaccount/`. |
| 305 | + |
| 306 | +**`AccessDenied` on a specific command** |
| 307 | + |
| 308 | + - The IRSA role policy does not include the required action. Add it to the IAM policy attached to the IRSA role. |
| 309 | + |
| 310 | +**`aws sts assume-role` rejected** |
| 311 | + |
| 312 | + - This command is intentionally blocked by the tool's validation layer. Use Option A (IRSA pointing directly at Account B) or Option B (named profiles via `~/.aws/config`) instead — see [Cross-Account AWS Access](#cross-account-aws-access). |
| 313 | + |
| 314 | +**Cross-account `AccessDenied`** |
| 315 | + |
| 316 | + - **Option A**: Verify Account A's OIDC provider is registered in Account B IAM (`aws iam list-open-id-connect-providers` in Account B). Verify the Account B role's trust policy references `arn:aws:iam::ACCOUNT_B_ID:oidc-provider/...` (not Account A's ARN) with the correct `sub` condition (`system:serviceaccount:<namespace>:<service-account>`). |
| 317 | + - **Option B**: Verify the Account B role's trust policy allows `arn:aws:iam::ACCOUNT_A_ID:role/kubeai-chatbot` to assume it, and that the Account A IRSA role has `sts:AssumeRole` permission targeting the Account B role ARN. |
0 commit comments