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.
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:
-
Reduced attack surface: By granting elevated permissions only when necessary and for limited time, exposure to potential security vulnerabilities is minimized.
-
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.
-
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 #
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) #
-
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.
-
The Lambda Grant function creates the temporary association between the user, target account, and Permission Set in IAM Identity Center.
-
The active permission state with the calculated expiration timestamp is stored in the Amazon DynamoDB state table.
-
The operation is recorded in the Amazon DynamoDB audit table with user, account, date/time, and result information.
-
In case of error, a notification is sent through Amazon SNS to the support team.
Automatic revocation flow (Revoke) #
-
Amazon EventBridge executes the Lambda Revoke function according to the configured cron rule (every hour).
-
AWS Lambda Revoke queries the Amazon DynamoDB state table to identify records whose expiration timestamp has been exceeded.
-
For each expired permission, the function removes the user/account/Permission Set association in IAM Identity Center.
-
The Amazon DynamoDB state table is updated by removing the processed records.
-
Each revocation operation is recorded in the audit table with the operation result.
-
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 #
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 newsletterReferences #
AWS IAM Identity Center:
Boto3 SDK: