Skip to main content
Just-In-Time (JIT) Access in AWS with Lambda and IAM Identity Center
Photo by Aron Visuals on Unsplash

Just-In-Time (JIT) Access in AWS with Lambda and IAM Identity Center

·1647 words·8 mins
AWS Aws Iam-Identity-Center Just-in-Time-Access Aws-Lambda Security
Sergio Cambelo
Author
Sergio Cambelo
Cloud Architect
Table of Contents

When defining the AWS access management strategy for a complex organization, the principle of least privilege is non-negotiable. However, environments are not static, and although a developer shouldn’t need access to the production environment a priori, when a critical incident arises, it’s necessary for that developer to access it immediately to resolve it. Traditionally, there are two approaches to allow such access. The first is to have pre-granted access by assigning a Permission Set with elevated permissions. This solution goes completely against the necessary minimum privilege, as it poses additional risk. The other solution involves configuring access for the developer at the moment, but this requires the availability of the team responsible for managing IAM Identity Center and requires coordination of more actors.

This second way of proceeding is known as Just-in-Time (JIT), which consists of granting elevated permissions temporarily only when necessary. In this article, we’ll see how to generate a fully automated JIT solution using native AWS services such as AWS Lambda and Identity Center itself.

Note: This article assumes the reader has knowledge about AWS IAM Identity Center and is familiar with the AWS services used in the solution: Lambda, DynamoDB, EventBridge, and SNS.

What is Just-in-Time (JIT) access
#

From the Cloud Governance perspective, Just-in-Time access serves as a control and audit mechanism to ensure that the necessary mechanisms are provided to operate the platform while guaranteeing security and traceability.

This way, it’s possible to provide teams working on the platform with the necessary freedom to carry out the tasks needed for maintenance and operation, ensuring that the organization’s security and compliance criteria are met.

Implementing JIT access provides significant benefits for an organization’s security and management:

  1. Reduced attack surface: By granting elevated permissions only when necessary and for limited time, exposure to potential security vulnerabilities is minimized.

  2. Compliance with the principle of least privilege: Users only have access to the resources they need at the exact moment they require them, eliminating unnecessary permanent permissions.

  3. Complete traceability and auditing: Each access request is recorded with detailed information about who, when, why, and for how long permissions were granted.

JIT Architecture with native AWS services
#

JIT architecture diagram showing the flow from user request through AWS Lambda to IAM Identity Center. The process includes: user requests elevated access, Lambda processes the request, validates permissions, creates temporary assignment in Identity Center, user receives temporary access, and finally Lambda automatically revokes access
Architecture of the automated JIT solution using AWS Lambda to manage temporary access in IAM Identity Center

Architecture components
#

IAM Identity Center: Centralized identity and access management service that allows managing users, groups, and Permission Sets. In our solution, it manages temporary access through dynamic Account Assignments.

AWS Lambda - Grant Function: Invoked from ITSM tools like ServiceNow, custom frontends, or Slack integrations. Validates input parameters, associates the user in IAM Identity Center with the requested account and elevated privileges Permission Set, records the result in the audit table, and stores the granted permission state. The expiration timestamp is calculated by adding the configured period (e.g., 24 hours) to the request date, and can be dynamic if specified in the invocation.

AWS Lambda - Revoke Function: Executed at regular intervals (e.g., every hour) through an Amazon EventBridge cron rule. Queries the Amazon DynamoDB state table to get records whose expiration timestamp has been exceeded, removes expired user/account/Permission Set associations, and records the result in the audit table.

Amazon EventBridge: Event bus service that schedules the execution of the Revoke function through configurable cron rules (e.g., every hour).

Amazon DynamoDB - State Table: Stores the current state of active JIT permissions, including the user, target account, and permission expiration timestamp to manage automatic revocation.

Amazon DynamoDB - Audit Table: Records all privileged access requests with user information, target account, request date and time, as well as the successful or failed result of each operation.

Amazon SNS: Notification service that sends alerts to the operations team when errors occur in any of the Lambda functions, allowing manual intervention when necessary.

Permission granting flow (Grant)
#

  1. The user requests elevated access through an ITSM tool (ServiceNow), custom frontend, or Slack integration. AWS Lambda Grant validates that all input parameters are correct and processes the request.

  2. The Lambda Grant function creates the temporary association between the user, target account, and Permission Set in IAM Identity Center.

  3. The active permission state with the calculated expiration timestamp is stored in the Amazon DynamoDB state table.

  4. The operation is recorded in the Amazon DynamoDB audit table with user, account, date/time, and result information.

  5. In case of error, a notification is sent through Amazon SNS to the support team.

Automatic revocation flow (Revoke)
#

  1. Amazon EventBridge executes the Lambda Revoke function according to the configured cron rule (every hour).

  2. AWS Lambda Revoke queries the Amazon DynamoDB state table to identify records whose expiration timestamp has been exceeded.

  3. For each expired permission, the function removes the user/account/Permission Set association in IAM Identity Center.

  4. The Amazon DynamoDB state table is updated by removing the processed records.

  5. Each revocation operation is recorded in the audit table with the operation result.

  6. In case of error during the process, notification is sent to the support team through Amazon SNS.

Error handling
#

In case of error in any of the flows, both Lambda functions send a notification through Amazon SNS to the operations team, allowing manual intervention to resolve the incident.

Lambda functions implementation
#

Important note: The code shown is for reference only to illustrate the concepts and flows described in this article. It should not be used directly in a production environment without thorough review, adaptation to the organization’s specific security requirements, and complete testing. It’s recommended to implement additional validations, more robust error handling, detailed logging, and compliance with corporate security policies.

Grant Function - Temporary access granting
#

The Lambda Grant function manages the temporary assignment of AdministratorAccess permissions to specific users. Below is example code showing how the function logic could be implemented.

import boto3
import json
import uuid
from datetime import datetime, timedelta

def lambda_handler(event, context):
    # AWS clients
    sso_admin = boto3.client('sso-admin')
    identitystore = boto3.client('identitystore')
    dynamodb = boto3.resource('dynamodb')
    sns = boto3.client('sns')
    
    # Input parameters
    user_email = event['user_email']
    account_id = event['account_id']
    duration_hours = event.get('duration_hours', 24)
    request_id = str(uuid.uuid4())
    
    try:
        # Get Identity Store ID and Instance ARN
        instances = sso_admin.list_instances()['Instances']
        instance_arn = instances[0]['InstanceArn']
        identity_store_id = instances[0]['IdentityStoreId']
        
        # Search user by email
        user_response = identitystore.list_users(
            IdentityStoreId=identity_store_id,
            Filters=[{
                'AttributePath': 'UserName',
                'AttributeValue': user_email
            }]
        )
        user_id = user_response['Users'][0]['UserId']
        
        # Search AdministratorAccess Permission Set
        permission_sets = sso_admin.list_permission_sets(
            InstanceArn=instance_arn
        )
        
        admin_permission_set_arn = None
        for ps_arn in permission_sets['PermissionSets']:
            ps_details = sso_admin.describe_permission_set(
                InstanceArn=instance_arn,
                PermissionSetArn=ps_arn
            )
            if ps_details['PermissionSet']['Name'] == 'AdministratorAccess':
                admin_permission_set_arn = ps_arn
                break
        
        # Create account assignment
        sso_admin.create_account_assignment(
            InstanceArn=instance_arn,
            TargetId=account_id,
            TargetType='AWS_ACCOUNT',
            PermissionSetArn=admin_permission_set_arn,
            PrincipalType='USER',
            PrincipalId=user_id
        )
        
        # Calculate expiration timestamp
        expiry_time = datetime.utcnow() + timedelta(hours=duration_hours)
        
        # Save state in DynamoDB
        state_table = dynamodb.Table('jit-access-state')
        state_table.put_item(
            Item={
                'assignment_id': f"{user_id}#{account_id}#{admin_permission_set_arn}",
                'user_id': user_id,
                'account_id': account_id,
                'permission_set_arn': admin_permission_set_arn,
                'expiry_timestamp': int(expiry_time.timestamp()),
                'created_at': datetime.utcnow().isoformat()
            }
        )
        
        # Record in audit table
        audit_table = dynamodb.Table('jit-access-audit')
        audit_table.put_item(
            Item={
                'request_id': request_id,
                'user_email': user_email,
                'account_id': account_id,
                'permission_set': 'AdministratorAccess',
                'action': 'GRANT',
                'status': 'SUCCESS',
                'timestamp': datetime.utcnow().isoformat(),
                'expires_at': expiry_time.isoformat()
            }
        )
        
        return {
            'statusCode': 200,
            'body': json.dumps({
                'message': 'Access granted successfully',
                'expires_at': expiry_time.isoformat()
            })
        }
        
    except Exception as e:
        # Record error in audit
        try:
            audit_table = dynamodb.Table('jit-access-audit')
            audit_table.put_item(
                Item={
                    'request_id': request_id,
                    'user_email': user_email,
                    'account_id': account_id,
                    'permission_set': 'AdministratorAccess',
                    'action': 'GRANT',
                    'status': 'ERROR',
                    'error_message': str(e),
                    'timestamp': datetime.utcnow().isoformat()
                }
            )
        except:
            pass
        
        # Send error notification via SNS
        try:
            sns.publish(
                TopicArn='arn:aws:sns:region:account:jit-access-errors',
                Subject='Error in JIT Grant function',
                Message=f'Error granting JIT access:\nUser: {user_email}\nAccount: {account_id}\nError: {str(e)}'
            )
        except:
            pass
        
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
        }

Revoke Function - Automatic access revocation
#

The Lambda Revoke function runs periodically to automatically remove expired JIT permissions:

import boto3
import json
import uuid
from datetime import datetime
from boto3.dynamodb.conditions import Attr

def lambda_handler(event, context):
    # AWS clients
    sso_admin = boto3.client('sso-admin')
    dynamodb = boto3.resource('dynamodb')
    sns = boto3.client('sns')
    
    current_timestamp = int(datetime.utcnow().timestamp())
    revoked_count = 0
    errors = []
    
    try:
        # Get Instance ARN
        instances = sso_admin.list_instances()['Instances']
        instance_arn = instances[0]['InstanceArn']
        
        # Query expired permissions
        state_table = dynamodb.Table('jit-access-state')
        response = state_table.scan(
            FilterExpression=Attr('expiry_timestamp').lt(current_timestamp)
        )
        
        expired_assignments = response['Items']
        
        for assignment in expired_assignments:
            try:
                # Delete account assignment
                sso_admin.delete_account_assignment(
                    InstanceArn=instance_arn,
                    TargetId=assignment['account_id'],
                    TargetType='AWS_ACCOUNT',
                    PermissionSetArn=assignment['permission_set_arn'],
                    PrincipalType='USER',
                    PrincipalId=assignment['user_id']
                )
                
                # Remove from state table
                state_table.delete_item(
                    Key={'assignment_id': assignment['assignment_id']}
                )
                
                # Record revocation in audit
                audit_table = dynamodb.Table('jit-access-audit')
                audit_table.put_item(
                    Item={
                        'request_id': str(uuid.uuid4()),
                        'user_id': assignment['user_id'],
                        'account_id': assignment['account_id'],
                        'permission_set': 'AdministratorAccess',
                        'action': 'REVOKE',
                        'status': 'SUCCESS',
                        'timestamp': datetime.utcnow().isoformat(),
                        'reason': 'EXPIRED'
                    }
                )
                
                revoked_count += 1
                
            except Exception as e:
                error_msg = f"Error revoking {assignment['assignment_id']}: {str(e)}"
                errors.append(error_msg)
                
                # Record error in audit
                try:
                    audit_table = dynamodb.Table('jit-access-audit')
                    audit_table.put_item(
                        Item={
                            'request_id': str(uuid.uuid4()),
                            'user_id': assignment.get('user_id', 'unknown'),
                            'account_id': assignment.get('account_id', 'unknown'),
                            'permission_set': 'AdministratorAccess',
                            'action': 'REVOKE',
                            'status': 'ERROR',
                            'error_message': str(e),
                            'timestamp': datetime.utcnow().isoformat()
                        }
                    )
                except:
                    pass
        
        # Send notification if there are errors
        if errors:
            try:
                sns.publish(
                    TopicArn='arn:aws:sns:region:account:jit-access-errors',
                    Subject='Errors in JIT Revoke function',
                    Message=f'Found {len(errors)} errors during revocation:\n\n' + '\n'.join(errors)
                )
            except:
                pass
        
        return {
            'statusCode': 200,
            'body': json.dumps({
                'message': f'Process completed. {revoked_count} permissions revoked, {len(errors)} errors',
                'revoked_count': revoked_count,
                'error_count': len(errors)
            })
        }
        
    except Exception as e:
        # General error in function
        try:
            sns.publish(
                TopicArn='arn:aws:sns:region:account:jit-access-errors',
                Subject='Critical error in JIT Revoke function',
                Message=f'Critical error in revocation function: {str(e)}'
            )
        except:
            pass
        
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
        }

Wrapping up
#

Implementing Just-in-Time access with native AWS services represents a perfect balance between security and operability. This solution allows organizations to maintain the principle of least privilege while providing the necessary flexibility to respond to critical incidents immediately and automatically.

The combination of AWS Lambda, IAM Identity Center, DynamoDB, and EventBridge offers a robust, scalable, and fully managed architecture that eliminates the need for external tools or manual processes. With complete traceability and automatic notifications, security teams maintain total control over privileged access while operational teams gain the necessary autonomy to resolve critical incidents.

Want to stay up to date with the latest trends in cloud architecture and AWS security? Subscribe to the newsletter and receive exclusive content about best practices, real use cases, and innovative solutions directly in your inbox.

Subscribe to newsletter

References
#

AWS IAM Identity Center:

Boto3 SDK:

Related

Enforcing Preventative Controls in a Multi-Account Environment
·1875 words·9 mins
AWS Aws Organizations Security Governance Scp Iam Multi-Account Cloud Governance
Global Security Inspection With AWS Cloud WAN
·2053 words·10 mins
AWS Aws Aws-Cloudwan Network-Function-Groups Service-Insertion Aws-Network-Firewall
AWS Cloud WAN: Simplify Your Global Network Infrastructure
·1804 words·9 mins
AWS Aws Networking Cloudwan Vpc Terraform Infrastructure-as-Code