> ## Documentation Index
> Fetch the complete documentation index at: https://docs.serval.com/llms.txt
> Use this file to discover all available pages before exploring further.

# AWS Role Ingestion

> How Serval discovers tagged IAM roles in your connected AWS accounts and grants users time-bound AWS sessions for them.

## About AWS Role Ingestion

AWS Role Ingestion is the part of Serval's AWS integration that discovers IAM roles in your connected AWS accounts and makes them requestable in Serval. Serval ingests every role carrying a tag with key `serval`, together with its policy documents and maximum session duration, and lets approved users start short-lived, just-in-time (JIT) AWS sessions for those roles. This page covers the role sync, the AWS-side setup, and the bridge service that mints JIT sessions. For connecting the AWS account itself, see the [AWS integration](/sections/integrations/aws) page; for access-policy configuration, see [AWS Role Access](/sections/access-management/configuration/aws-role-access).

**Authentication:** Cross-account IAM role (STS AssumeRole with an External ID) for role ingestion. JIT sessions use OIDC federation (STS AssumeRoleWithWebIdentity) against your Okta identity provider - brokered by Serval's cloud endpoints on Serval cloud, or by the self-hosted bridge service that runs alongside your worker on self-hosted deployments.

**Data sync:** Scheduled full sync every 4 hours (with a 30-minute offset). There is no delta sync, so changes in AWS can take up to 4 hours to appear in Serval.

## What the AWS Role Ingestion integration enables

| Capability                       | Description                                                                                                                                                                                                                                                                                                                                     |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| IAM role ingestion               | The built-in workflow "Fetch AWS IAM Roles for Resource Sync" ingests every IAM role tagged with key `serval` from each connected account, including inline policy documents, attached managed policy documents, and the role's maximum session duration.                                                                                       |
| Assume Role entitlement          | Each ingested role surfaces exactly one requestable entitlement named "Assume Role" ("Ability to assume this AWS IAM role") in Serval's access-request flow.                                                                                                                                                                                    |
| Temporary (JIT) AWS sessions     | Approved users mint short-lived AWS credentials (Access Key ID, Secret Access Key, Session Token) for a role using their OIDC identity (Okta in the standard setup). Session length is the shorter of the remaining approved access time and the role's maximum session duration, defaulting to 1 hour, with an AWS-enforced 15-minute minimum. |
| Deferred deprovision timer       | The granted access window starts counting down when the user opens their first AWS session, not when the request is approved.                                                                                                                                                                                                                   |
| CloudTrail-attributable sessions | JIT sessions minted through the self-hosted bridge use the requester's email (truncated to 64 characters) as the session name, so activity in CloudTrail is attributable per user. Sessions minted through Serval cloud appear under the shared session name `serval-user-access`.                                                              |
| Pre-built install workflows      | The AWS integration ships two installable workflows: "Create AWS IAM Role" (installer approval by default) and "List AWS Organization Accounts" (no approval by default).                                                                                                                                                                       |

## Get your credentials

Set up an ingestion role in each AWS account Serval should read roles from. If you want users to mint AWS sessions, also complete the JIT setup. AWS documents the cross-account pattern in [Delegate access across AWS accounts using IAM roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user.html) and the External ID pattern in [How to use an external ID for third-party access](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html).

<Tabs>
  <Tab title="Ingestion role (required)">
    <Steps>
      <Step title="Create the role">
        In the AWS console, go to **IAM > Roles > Create role** and choose **AWS account** as the trusted entity type.
      </Step>

      <Step title="Set the trusted account and External ID">
        Enter the AWS account ID that performs the role assumption: `992382851720` on Serval cloud, or the account your own worker runs in if self-hosted. Tick **Require external ID** and paste the External ID shown in Serval's Connect AWS dialog.
      </Step>

      <Step title="Skip permissions and create">
        Skip the permission-policies step for now (you will add an inline policy next), then name and create the role.
      </Step>

      <Step title="Add the TagSession trust statement">
        Open the role, go to **Trust relationships > Edit trust policy**, and add a second statement allowing the action `sts:TagSession` for the same account principal. <Warning>The trust policy AWS generates by default only allows `sts:AssumeRole` with the External ID condition. Without the separate `sts:TagSession` statement, Serval's role assumption fails.</Warning>

        A complete trust policy looks like this (replace `<WORKER_ACCOUNT_ID>` with the trusted account from the previous step and `<EXTERNAL_ID>` with the External ID from Serval's Connect AWS dialog):

        ```json theme={null}
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "AWS": "arn:aws:iam::<WORKER_ACCOUNT_ID>:root"
              },
              "Action": "sts:AssumeRole",
              "Condition": {
                "StringEquals": {
                  "sts:ExternalId": "<EXTERNAL_ID>"
                }
              }
            },
            {
              "Effect": "Allow",
              "Principal": {
                "AWS": "arn:aws:iam::<WORKER_ACCOUNT_ID>:root"
              },
              "Action": "sts:TagSession"
            }
          ]
        }
        ```
      </Step>

      <Step title="Add the read-only inline policy">
        Under **Permissions > Create inline policy**, allow these seven actions on Resource `*`: `iam:ListRoles`, `iam:GetRole`, `iam:ListRolePolicies`, `iam:GetRolePolicy`, `iam:ListAttachedRolePolicies`, `iam:GetPolicy`, `iam:GetPolicyVersion`.

        ```json theme={null}
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": [
                "iam:ListRoles",
                "iam:GetRole",
                "iam:ListRolePolicies",
                "iam:GetRolePolicy",
                "iam:ListAttachedRolePolicies",
                "iam:GetPolicy",
                "iam:GetPolicyVersion"
              ],
              "Resource": "*"
            }
          ]
        }
        ```
      </Step>

      <Step title="Tag the roles Serval should manage">
        Add a tag with key `serval` (the value may be empty) to every role Serval should ingest and grant. Untagged roles are ignored entirely.
      </Step>

      <Step title="Self-hosted only: grant the worker its STS permissions">
        Self-hosted workers perform the cross-account role assumption themselves, so the worker pod needs AWS credentials. The standard setup uses [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) (see the [agent setup guide](https://docs.aws.amazon.com/eks/latest/userguide/pod-id-agent-setup.html)): create a Kubernetes service account for the worker (the standard setup names it `serval-worker-sa` - the worker chart does not create one for you), reference it from the worker deployment, and add an EKS Pod Identity association binding it to an IAM role that allows `sts:AssumeRole` and `sts:TagSession` on Resource `*`. This is required for ingestion itself, even if you never set up JIT sessions.

        Example Terraform for the worker IAM role, its assume-role/permission policies, and the Pod Identity association:

        ```hcl theme={null}
        # The IAM Role to give to the Serval worker
        resource "aws_iam_role" "this_service_role" {
          name               = "eks-pod-identity-serval-worker"
          assume_role_policy = data.aws_iam_policy_document.assume_role.json
        }

        # Trust policy which allows the EKS pod identity system to use this role
        data "aws_iam_policy_document" "assume_role" {
          statement {
            effect = "Allow"

            principals {
              type        = "Service"
              identifiers = ["pods.eks.amazonaws.com"]
            }

            actions = [
              "sts:AssumeRole",
              "sts:TagSession"
            ]
          }
        }

        # Policy document and attachment so the role can assume roles in other accounts
        data "aws_iam_policy_document" "policy" {
          statement {
            effect = "Allow"
            actions = [
              "sts:AssumeRole",
              "sts:TagSession"
            ]
            resources = ["*"]
          }
        }
        resource "aws_iam_policy" "policy" {
          name        = "assume-role-policy-serval-worker"
          description = "Policy to allow assuming roles in other AWS accounts."
          policy      = data.aws_iam_policy_document.policy.json
        }
        resource "aws_iam_role_policy_attachment" "policy_attachment" {
          role       = aws_iam_role.this_service_role.name
          policy_arn = aws_iam_policy.policy.arn
        }

        # Association between the role and the "serval-worker-sa" service account
        resource "aws_eks_pod_identity_association" "pod_identity_association" {
          cluster_name    = "<your_cluster_name>"
          namespace       = "<serval_namespace>"
          service_account = "serval-worker-sa"
          role_arn        = aws_iam_role.this_service_role.arn
        }
        ```

        The matching service account in the worker's Kubernetes namespace:

        ```yaml theme={null}
        ---
        apiVersion: v1
        kind: ServiceAccount
        metadata:
          name: serval-worker-sa
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="JIT access via the Serval bridge (self-hosted)">
    The bridge is a small service that runs alongside your self-hosted worker. Its Kubernetes Service is named `serval-bridge` (port 80, forwarding to 8080) and it must be reachable by your users over your VPN. It exchanges each user's Okta identity for temporary AWS credentials.
    <Note>On Serval cloud (no self-hosted worker), JIT sessions are brokered by Serval's hosted endpoints instead - no bridge deployment is needed, and sessions appear in CloudTrail under the name `serval-user-access`. The AWS-side identity provider and trust-policy steps below still apply; ask your Serval contact for the OIDC values to use.</Note>

    <Steps>
      <Step title="Create an Okta OIDC web app">
        In Okta Admin, go to **Applications > Create App Integration** and create an OIDC web application following [Okta's web-app sign-in guide](https://developer.okta.com/docs/guides/sign-into-web-app-redirect/go/main/). Set the sign-in redirect URI to `/oidc/auth/callback` on your bridge host, and note the Client ID and Client Secret. The bridge requests the `openid`, `profile`, and `email` scopes at login, and the token's email claim must match the user's Serval login email.
        <Note>Only `/oidc/auth/login` and `/oidc/auth/callback` exist on the bridge. Leave any Okta sign-out redirect unused - there is no logout route.</Note>
      </Step>

      <Step title="Store the credentials for the bridge">
        Provide the Client ID and Client Secret to your Serval contact for the bridge deployment. They are supplied to the bridge through the `bridge-secrets` Kubernetes secret, which only the bridge deployment references.
      </Step>

      <Step title="Register the Okta app as an AWS Identity Provider">
        In each AWS account, go to **IAM > Identity providers > Add provider**. Set the provider URL to your Okta instance URL and the audience to the OIDC app's Client ID.
      </Step>

      <Step title="Add the federated trust statement to every grantable role">
        On each role users should be able to assume, add a trust-policy statement with the Federated principal `arn:aws:iam::<ACCOUNT_ID>:oidc-provider/<IDP_ISSUER_URL>`, the action `sts:AssumeRoleWithWebIdentity` (see the [AWS API reference](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html)), and a StringEquals condition that `<IDP_ISSUER_URL>:aud` equals `<SERVAL_OIDC_APP_CLIENT_ID>`. The placeholders: `<ACCOUNT_ID>` is the AWS account the role lives in, `<IDP_ISSUER_URL>` is your Okta instance URL as registered on the identity provider, and `<SERVAL_OIDC_APP_CLIENT_ID>` is the Okta OIDC app's Client ID.
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Connect in Serval

Role ingestion uses the standard AWS connection - follow the connect flow on the [AWS integration](/sections/integrations/aws) page to open the Connect AWS dialog, then fill it in as follows.

<Steps>
  <Step title="Copy the read-only values">
    The dialog displays two read-only badges: **Serval AWS Account ID** (`992382851720`) and **External ID**. Use both in your ingestion role's trust policy. The External ID is a deterministic 64-character value derived from your team, so it is identical on every reconnect and safe to hardcode in Terraform trust policies.
  </Step>

  <Step title="Your AWS Account ID (required)">
    Enter the 12-digit ID of the account you are connecting. There is no format validation; this value becomes the connection's service instance ID.
  </Step>

  <Step title="Your AWS Account Name (required)">
    A display label for the account - this is what the connection is called in Serval.
  </Step>

  <Step title="Role ARN (required)">
    The ARN of the ingestion role you created. The dialog performs no ARN-format validation, so double-check the value before submitting - a typo'd ARN appears to connect successfully and only surfaces when the "Test AWS Connection" health check or the next scheduled sync fails. Submit stays disabled until all three required fields are non-empty.
  </Step>
</Steps>

<Warning>Self-hosted deployments: the dialog always displays Serval's cloud account ID, but your ingestion role must trust the AWS account your own worker runs in, not `992382851720`.</Warning>

## Verifying the connection

Run the connection health checks after connecting.

**Test AWS Connection** - confirms Serval can assume your cross-account role and read IAM. This is the authoritative check before debugging role ingestion.

* Success: "Successfully authenticated with AWS"
* Failure (role assumption fails): "Unable to authenticate with AWS. Please verify your cross-account role ARN and external ID are correct."
* Failure (role assumed, but the inline policy is missing the IAM read permission the sync also needs): "Connection successful, but IAM role lacks iam:ListRoles permission."

**List AWS Organization Accounts** - confirms Organizations API permissions for the optional account-listing workflow.

* Success: "Successfully retrieved \[number] accounts from AWS Organizations"
* Failure (missing access): "Unable to access AWS Organizations. Ensure the IAM role has organizations:ListAccounts permission."
* Failure (permission only): "Connection successful, but IAM role lacks organizations:ListAccounts permission."
* Failure (standalone account): "This AWS account is not part of an AWS Organization." This is expected for accounts outside an AWS Organization and is not a connectivity problem.

<Tip>A green "Test AWS Connection" with zero roles appearing in Serval almost always means tagging, not connectivity: only roles tagged with key `serval` are ingested, and a newly tagged role can take up to 4 hours to show up on the next scheduled sync.</Tip>

## Gotchas and troubleshooting

<AccordionGroup>
  <Accordion title="Only roles tagged 'serval' are ingested">
    The sync filters on tag key `serval` exactly; the value is ignored and may be empty. An account full of roles syncs zero resources until tagging is done, and untagging a role removes it on the next full sync. Check tags before debugging the connection.
  </Accordion>

  <Accordion title="Changes can take up to 4 hours to appear">
    Role ingestion is a full sync every 4 hours (30-minute offset) with no delta sync. The same lag applies to session-length changes: JIT session caps are read from Serval's ingested copy of the role, so raising a role's maximum session duration in AWS only takes effect after the next sync.
  </Accordion>

  <Accordion title="The default AWS trust policy is insufficient - add sts:TagSession">
    The ingestion role's trust policy needs a second statement allowing `sts:TagSession` for the trusted account principal alongside the `sts:AssumeRole` statement with its External ID condition. Workers running under EKS Pod Identity carry transitive session tags, so role assumption fails against a trust policy without TagSession.
  </Accordion>

  <Accordion title="JIT session length is not a flat 3 hours">
    Older guidance mentioned a 3-hour cap; that is stale. The session lasts the shorter of the remaining approved access time and the role's ingested maximum session duration. If that duration is missing, the default is 1 hour, and AWS enforces a 15-minute minimum (shorter requests are rounded up). On a failed attempt, the session broker retries once with the 1-hour default.
  </Accordion>

  <Accordion title="The access clock starts at first session, not at approval">
    Deprovisioning is deferred until after first access: a user's granted duration begins when they start their first AWS session, not when the request is approved.
  </Accordion>

  <Accordion title="A role showing no policies may be a permissions gap">
    Policy-fetch errors are logged but do not fail the sync - the role is still ingested with whichever policy lists succeeded, possibly none. A role with no policies in Serval may indicate missing `iam:GetRolePolicy`, `iam:GetPolicy`, or `iam:GetPolicyVersion` permissions rather than a truly policy-less role.
  </Accordion>

  <Accordion title="Older setup guides list six unneeded IAM actions">
    `iam:ListRoleTags`, `rds:DescribeDBInstances`, `rds:DescribeDBClusters`, `ec2:DescribeInstances`, `eks:DescribeCluster`, and `eks:ListClusters` appeared in earlier versions of the inline policy, but no shipped ingestion workflow uses them (tags come back on `iam:GetRole`). Only the seven actions listed above are required for role ingestion. The optional install workflows have their own needs - `organizations:ListAccounts` for "List AWS Organization Accounts", and `iam:CreateRole` plus `iam:PutRolePolicy` for "Create AWS IAM Role" - and your own custom workflows may need more.
  </Accordion>

  <Accordion title="CloudTrail shows many short sessions - that is normal">
    Ingestion assumes the role with a fresh UUID session name and a 15-minute duration on every credential refresh, so frequent short UUID-named sessions are expected. JIT sessions minted through the self-hosted bridge are named with the requester's email truncated to 64 characters, giving per-user attribution; sessions minted through Serval cloud appear under the shared name `serval-user-access`.
  </Accordion>

  <Accordion title="The bridge service is named serval-bridge, not svbridge-api">
    The Kubernetes Service is `serval-bridge` (port 80, forwarding to 8080); `svbridge-api` is the container name inside the deployment. The bridge must be reachable by users over your VPN, and its OIDC secrets live in the `bridge-secrets` Kubernetes secret.
  </Accordion>

  <Accordion title="There is no /oidc/auth/logout route">
    The bridge serves `/oidc/auth/login` and `/oidc/auth/callback` only. Configure the Okta sign-in redirect to `/oidc/auth/callback` and treat any sign-out redirect URI as unused.
  </Accordion>

  <Accordion title="Okta login email must match the Serval account email">
    During the OIDC exchange, the email claim in the Okta token is compared to the user's Serval login email. A mismatch fails with "Account mismatch - You tried to log in with email \[Okta email] but your Serval account uses \[Serval email]. Please ensure you are using the same login email for your Serval account and your auth provider." Users with different aliases in Okta and Serval cannot start sessions until the emails are aligned.
  </Accordion>

  <Accordion title="Where a mistyped Role ARN actually surfaces">
    The connect dialog accepts any non-empty Role ARN, so a typo in the ingestion role's ARN shows up later as a failed "Test AWS Connection" health check or a failed scheduled sync - not at session start. Separately, the self-hosted bridge validates the ARN of the role being granted and rejects any value that does not start with `arn:` with the error "invalid role ARN format"; since that ARN is ingested from AWS itself, hitting this error usually indicates corrupted or stale ingested data rather than a setup typo.
  </Accordion>

  <Accordion title="Session initiation fails with a bridge connection error">
    On self-hosted deployments the browser talks to the bridge directly, so "Unable to connect to the bridge service" errors usually mean the user is not on the VPN, or third-party cookies are blocked when the bridge sits behind a service like Cloudflare Zero Trust. See [Resolving Cookie Issues for AWS Session Granting](/sections/self-hosting/deployment/onprem/ResolvingCookieIssuesforAWSSessionGranting) for browser-specific fixes.
  </Accordion>

  <Accordion title="AWS traffic does not go through Serval's HTTP proxy">
    AWS request signing happens inside the worker, so the worker calls AWS endpoints (`*.amazonaws.com`) directly with short-lived 15-minute credentials instead of routing through Serval's host-allow-listed proxy. If you restrict the worker's egress, allow `iam.amazonaws.com`, `organizations.us-east-1.amazonaws.com`, and `sts.us-west-1.amazonaws.com` (the self-hosted bridge likewise calls `sts.us-west-1.amazonaws.com`). Custom workflows that use other AWS submodules (EC2, S3, Lambda, RDS) call those services' regional endpoints too.
  </Accordion>
</AccordionGroup>

***

Need help? Contact **[support@serval.com](mailto:support@serval.com)** for assistance with your AWS Role Ingestion integration.
