Skip to main content

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 page; for access-policy configuration, see 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

CapabilityDescription
IAM role ingestionThe 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 entitlementEach 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 sessionsApproved 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 timerThe granted access window starts counting down when the user opens their first AWS session, not when the request is approved.
CloudTrail-attributable sessionsJIT 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 workflowsThe 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 and the External ID pattern in How to use an external ID for third-party access.
1

Create the role

In the AWS console, go to IAM > Roles > Create role and choose AWS account as the trusted entity type.
2

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

Skip permissions and create

Skip the permission-policies step for now (you will add an inline policy next), then name and create the role.
4

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.
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.
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):
{
  "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"
    }
  ]
}
5

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.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iam:ListRoles",
        "iam:GetRole",
        "iam:ListRolePolicies",
        "iam:GetRolePolicy",
        "iam:ListAttachedRolePolicies",
        "iam:GetPolicy",
        "iam:GetPolicyVersion"
      ],
      "Resource": "*"
    }
  ]
}
6

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

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 (see the agent setup guide): 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:
# 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:
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: serval-worker-sa

Connect in Serval

Role ingestion uses the standard AWS connection - follow the connect flow on the AWS integration page to open the Connect AWS dialog, then fill it in as follows.
1

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

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

Your AWS Account Name (required)

A display label for the account - this is what the connection is called in Serval.
4

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

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

Gotchas and troubleshooting

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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 for browser-specific fixes.
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.

Need help? Contact support@serval.com for assistance with your AWS Role Ingestion integration.