Implementing Attribute Based Access Control (ABAC) with AWS Transfer Family SFTP Servers

I expect most people reading this will be familiar with Role Based Access Control (RBAC) for managing resource access where permissions are associated with roles and roles are assigned to users.  This works well for situations where groups of users have the same privileges and the number of roles is relatively small such as “administrator“, “developer“, “tester“, etc.

An alternative approach is Attribute Based Access Control (ABAC) where access to a particular resource is controlled by one or more attributes that a user possesses.  This technique is applicable where access control is based on a combination of characteristics of a user, so not only job-role as above, but maybe project as well, which can quickly result in an explosion of policies.   Resources themselves can have ResourceTags applied, with the security policies then simply enforcing that the values of those specific tags match the user’s attributes.  This significantly reduces the number and complexity of the policies being managed.

As we will see below, ABAC can also be used where controlling access to many individual resources in arbitrary combinations is required.  Here one or more resource specific attributes can be applied to the user and referenced in the policies attached to the resources.

Although less frequently used, implementing ABAC on AWS is well documented … Attribute-Based Access Control – AWS Identity, What is ABAC for AWS? – AWS Identity and Access Management and Practicing the Principle of Least Privilege – DEV Community.

The first two describe the fundamental principles of ABAC with the last one detailing an implementations where Custom Attributes are defined for a User Pool in Cognito which can then be mapped to Custom Claims in a Cognito Identity Pool and then referenced as PrincipalTags in IAM Policies as such as an S3 bucket policy.  

This is great where temporary credentials can be obtained and passed in the API call for an AWS service such as an S3 list object request …

However, a recent client requirement to demonstrate using an ABAC approach to control access to individual folders within S3 buckets which would be accessed via an AWS Transfer Family SFTP Server turned out to be somewhat more challenging!

AWS Transfer Family Servers can maintain users within the service itself, within Active Directory or by using a Custom Identity Provider (custom idp) to access any Identity Provider with an API.  This could be another AWS service such as Cognito or Secrets Manager, or a third party identity provider such as Auth0.  The custom idp itself can be either an AWS Lambda Function or an Amazon API Gateway call.  In this case the client wanted users to be maintained in Cognito which appeared straightforward as this seemed to be close to the scenario described in the DEV Community article above.

However, after initially successfully creating a lambda function custom idp which was capable of retrieving a user from Cognito via its associated Identity Pool with their attributes automatically mapped into PrincipalTags, a significant hurdle was discovered as the interface between the Transfer Family Server and the lambda function is quite basic (see Using AWS Lambda to integrate your identity provider – AWS Transfer Family).  

When a user attempts to log in, the Transfer Family Server passes the following JSON payload to the lambda function …

… which is fine.  The lambda function can use this information to authenticate the user as required.  The issue arose when looking at the response.  The only element which is used to communicate the privileges of the user back to the server is the ARN of an IAM Role which the server then uses to perform an assume role operation before accessing S3.

After a lot of head scratching and discussions with colleagues to try and find some way to propagate attributes from Cognito without success, I came up with a different approach.  Would it be possible to programmatically maintain an individual role per user, applying tags directly to the role which would hopefully then be available to the IAM policy to inspect and grant the appropriate access accordingly?

Manually creating a role with a tag and passing this back to the Transfer Family Service from the lambda function was successful, so then it was just a case of enhancing the lambda function to read the details of a user from Cognito including their custom attributes and create or update a role for that user with tags corresponding to the attributes that had been defined for that user in Cognito.

I decided to implement a scheme where the value of the attribute must match the value ‘allow’ (case insensitively) in order to grant access to a particular folder.  The S3 bucket policy itself contains a statement for each folder being protected (line 7) that specifies the PrincipleTag name and the required value (line10).  Each statement must also contain a Principle which cannot contain a wildcard to match a partial entity, so instead all principles are accepted (line 4) but the ArnLike condition restricts this to only allow roles with the chosen prefix (line 13).

The first time a user logs in the lambda function creates a role for that user.  One slightly surprising discovery was that it is not possible to assume a newly created role for around 6 seconds, even in the thread which created the role.  The lambda function therefore has a fairly arbitrary looking sleep in the create role function.  The function then compares the user’s attributes defined in Cognito with the tags on the role, removing any which are no longer defined, updating those where the value has changed and adding any new attributes.  The ARN of this role is then returned to the SFTP server which assumes the role and accesses S3.

The example code below is a simplified version without the integration with users in Cognito.  Instead the AWS System Manager Parameter Store is used to store a parameter containing a JSON string with the attribute definitions.  Also the only authentication, other than a user must have an entry in the Parameter Store which matches their username, is a password check to a hardcoded value of ‘welcome123’.  However implementing authentication with Cognito or looking up a secret in the AWS Secrets Manager would be straightforward.

One limitation is that this approach requires one role per user and the number of roles that can be created in an AWS account is not unlimited.  The default quota is 1000 roles per account with a maximum of 5000, so this approach may not work if you have a very large number of users.

In the right context ABAC can reduce the complexity of a security implementation, decreasing the possibility of accidental exposure of resources to unauthorised parties and reducing the associated maintenance overhead.

Next I will run through all the steps required to set up a functioning ABAC implementation with a Transfer Family SFTP Server.

AWS Transfer Family SFTP Server Setup

Create a Lambda Function

A lambda function must be supplied during the setup of the AWS Transfer Family SFTP server, so create a placeholder function which will be populated later.  In the AWS Console navigate to the Lambda Service and click on Create function.  The Author From Scratch option should already be selected so enter ‘abac-custom-identity-provider’ as the  Function Name, select ‘Node.js 20.x’ for the Runtime, ‘arm64’ for the Architecture and click Create Function.

Create a Transfer Family Server

Now the AWS Transfer Family Server can be created.  Start by navigating to the Transfer Family Service and click on Create Server.  Select SFTP for the Protocol and click Next.
For the Identity Provider select the Customised Identity Provider option, pick the lambda function created earlier (‘abac-custom-identity-provider’), select ‘Password ONLY’ as the Authentication Method and click Next.  To implement Public Key Authentication the lambda function simply retrieves the SSH key or keys associated with the user being authenticated and returns them to the SFTP Server which uses them to authenticate the incoming connection.
The SFTP Server will be publicly accessible so no changes are required on this screen, just click Next.
S3 will be used for the file storage so no changes are required on this screen either, just click Next.
Again for simplicity no changes will be made to the default settings for Logging, etc., so just click Next.
Check the settings on the final Review page and click Create Server.

Create an S3 Bucket

The S3 bucket used for this example will have a number of folders with access to each folder controlled by a different attribute.Navigate to the S3 Service and click Create Bucket.  Enter ‘abac-example’ for the Bucket Name.  The default values will be used for all other settings, in particular Amazon S3 Managed Keys will be used for server side object encryption and Bucket Versioning will not be used.Note: if AWS Key Management Service keys are used for encryption then the execution role (see below) will additionally need to be allowed to perform the kms:Decrypt action on that key.
To demonstrate the control that can be achieved with ABAC, create four folders in the bucket named subscription-1 to subscription-4, each containing a simple text file.
In the Permissions tab for this bucket, add the bucket policy from the appendix below.Since a Principal must be present in each statement, a restriction where the role ARN must match the pattern ‘abac-example-*’ is used.The first statement allows requests to list objects in the bucket so all folders will be visible.Each of the remaining statements relates to an individual folder in the bucket and applies the actual attribute based access control.  Here, the S3:GetObject action is only allowed if the request has a PrincipalTag which matches the relevant attribute name and has a value of ‘allow’.Note: If bucket versioning has been enabled then in addition to s3:GetObject, the statement for each folder will also need to include the action s3:GetObjectVersion.

Update the Lambda Function

Returning to the lambda function created earlier, selecting the Configuration tab and the General Configuration item on the left hand side, update the timeout to Timeout to 15 seconds.
Still in the Configuration tab, select the Permissions item and click on the Role Name link in the Execution Role section.  This will open the Role in the IAM service screen.
Click on the + symbol next to the Policy Name in the Permissions Policies section which will display the JSON definition of this policy.
Click on the Edit button and add the following statements to the end of the policy …  

  {
      “Effect”: “Allow”,
      “Action”: [
         “iam:GetRole”,
         “iam:UntagRole”,
         “iam:TagRole”,
         “iam:CreateRole”,
         “iam:AttachRolePolicy”
      ],
      “Resource”: [ “arn:aws:iam::111122223333:role/
abac-example-*”
      ]
   },
   {
      “Effect”: “Allow”,
      “Action”: “ssm:GetParameter”,
      “Resource”: [
“arn:aws:ssm:eu-west-1:111122223333:
parameter/*”
      ]
   }

Click Next and Save Changes.
These give the lambda function permissions to maintain the roles for the users associated with this example and to read parameters from the AWS Systems Manager Service which is used in this example implementation to store the attribute configuration for each user.

Note: if AWS Key Management Service keys are used for encryption then the execution role will additionally need to be allowed to perform the km: Decrypt action on that key.  It is here that any other permissions that the lambda function requires must be added.  For instance access to Cognito or Secrets Manager actions if one of these is used to store user details, or permissions to an S3 bucket storing SSH keys if the public key authentication scheme is used by the Transfer Family SFTP Server to authenticate users.
Remaining in the Configuration tab and the Permissions item, scroll down to the Resource-Based Policy Statements section and click on the Add Permissions button
Select the AWS Service option, select a Service of Other and enter a name for the Statement ID.  Enter ‘transfer.amazonaws.com’ for the Principle and set the Source ARN to the ARN of the Transfer Family SFTP server.  The format for this is ‘arn:aws:transfer: ${region}:${account}:server/${serverId}’.  Finally select lambda:InvokeFunction for the Action and click Save.
Finally in the Code tab, add the code from the Appendix below and click Deploy.

Store Attribute Settings in System Manager Parameters

Navigate to the System Manager Service, select the Parameter Store from the left hand navigation menu and click Create Parameter.
The Name for the parameter is the username the user will use when logging into the Transfer Family SFTP Server.  The Type and Data Type can be left as default and for the Value, enter a JSON format string of key/value pairs where the key names correspond to the attribute names configured in the S3 bucket policy and the value is the string ‘allow’ (case insensitive) to give the user access to the protected folder or any other value to explicitly deny access.  Where an attribute is not present in the data then the user is implicitly denied access to the protected resource.Next create parameter entries for each user.

Testing

Logging into the Transfer Family SFTP Server for the first time, a list of all the folders in the bucket is displayed.  The initial login takes a little longer as the role for the user must be created which requires a 6 second delay to allow the role to be propagated within AWS before it can be assumed by the SFTP Server.
Looking at the logs entries for the Lambda Function in Cloudwatch, a new role is created and the appropriate Tags added to that role…
Refreshing the list of IAM Roles, the newly created role for the user ‘abac-user-3’ is now visible and the Tags applied to that role can be inspected.Note: Even if the tags or their values are manually altered here, when the user next logs in they will be reset to the values defined in the Parameter Store.Also, the console will not update if the tags or their values are updated by the lambda function.  A page refresh (F5) must be performed to ensure the values observed are accurate.
Attempting to access the folder for subscription-1 returns an access denied error …
… but subscription-2 is accessible.
To verify the tags on the role are updated as expected when changes are made the the Parameter Store, the values of the attribute for subscription-1 will be changed to ‘allow’, subscription-2 will be removed and an attribute for subscription-3 will be added with a value of ‘allow’.
After logging in again, the log entries show the existing role is retrieved, the tag for subscription-2 is removed and the tags for subscription-1 and subscription-3 are updated…
Now the folder for subscription-1 is accessible …
… but subscription-2 no longer is.

Appendices  

S3 Bucket Policy

Lambda Function Execution Role Permissions Policy

Lambda Function Code

By Jeremy Gosling, Consultant at Estafet

Stay Informed with Our Newsletter!

Get the latest news, exclusive articles, and updates delivered to your inbox.