This project contains contains code to automate the deployment of a SAML proxy that is configured to "Proxy" SAML assertions between an identity provider and the AWS Service Provider.
Out of the box, the SAML proxy is configured to support AppStream customers that need to convert the format of the subject (NameID) from [email protected]
to DOMAIN.COM\username
when their IdP cannot support this transformation. This specific transformation was needed for a customer that uses an MIT Kerberos cross-realm trust that is case-sensitive and would only work with the SAMAccountName
format, but their IdP
could only send the NameID
in UserPrincipalName
format with a lowercase domain. Though this is likely an edge case, the proxy could also be configured to support a variety of other scenarios, such taking a a SAML attribute receivedd from the source IdP and passing that back to the AWS SP as the NameID
, or enriching the response assertion by injecting addtional attributes from an external source (i.e. LDAP)
The saml-project project uses SATOSA, an identity proxy application largely based on pysaml2. Saml-proxy includes a customized SATOSA SAML Frontend plugin that accepts unsolicited requests as well as a custom SATSOA microservice that can parse UserNamePrincipal
([email protected]
) formatted NameID
and convert it to SAMAccountName
(DOMAIN.COM\username
) format with (optionally) an uppercase domain. The custom SAML Frontend plugin and microservice are NOT part of the SATOSA code base. The code for these customzations can be found in satosa/micro_services
and satosa/frontends
folders of this repo.
- Fully automated - The setup of Saml-Proxy is full automated. Simply zip up the contents of this repo and upload to
code.zip
in an S3 bucket, then run thecfn-pipeline.yml
template. A CodePipeline project is created that handles the rest. Details below. - Minimal infrastructure - The app runs as a Fargate task behind an Application Load Balancer, within its own isolated VPC.
- Simple setup - Configured out of the box to proxy requests to the AWS SP from your IdP.
- Secure - The private key used for SAML Metadata is automatically generated and securely stored in Secrets Manager.
- Customizable - After deploying the solution, you can connect to the CodeCommit Repo and further customize the SATOSA configuration as desired. Can be used with other SPs as well as a SAML-OIDC or SAML-Social Login solution. See SATOSA docs for details. Commit changes to the repo and CodePipeline will automatically re-deploy.
- An SAML2 Identity Provider (IdP) that will be used for authentication. Once installed, you will need to share the SAML Proxy's metadata with your IdP.
- An S3 bucket to store the cloudformation templates
- The appropriate IAM permissions to create all required infrastructure resources AND access the S3 bucket.
- An SSL certificate provisioned in AWS Certificate Manager - This will be used on the HTTPS listener of the Load Balancer
- (Optional) A Route53 hosted zone in the account, if you want to automatically create a resource record for the SAML Proxy.
ℹ️ Important |
---|
During the deployment, the pipeline will automatically create a self-signed certificate and private key for the SP and IdP metadata and store them in Secrets Manager. If you want to provide your own certificate, you can edit the secrets in Secrets Manager after the initial deployment, then stop any running tasks in the ECS service. |
-
Create a certificate in AWS Certificate Manager, if you haven't yet
-
If you want to automatically create a DNS Record in Route53, ensure you already have a Hosted Zone created.
-
Download
code.zip
(an archive of the repo located in the root folder) and upload it to the S3 bucket -
Create an S3 bucket and copy the files in the cloudformation directory into it
-
Download
cfn-templates/cfn-pipeline.yml
-
In the CloudFormation console, go to Create Stack, select Upload a template file and browse to the
cfn-pipeline.yml
template you downloaded in the previous step. Click Next. -
For Stack Name, give the stack a meaningful name (i.e. saml-proxy). In the Parameters section, provide values for each paramter and click Next.
Note: If you don't have a Route53 Hosted Zone in the account or use an external DNS provider, skip the Route53 DNS section.
-
At the Configure stack options page, scroll to the bottom and click Next.
-
At the Review options page, scroll to the bottom and check the box next to I acknowledge that WS cloudFormation might create IAM resources and click Create stack.
-
In the Cloudformation console, you can monitor the progress of the stack deployment. When the deployment is complete, You can click the Outputs tab, then click the link next the PipelineUrl to be taken to the CodePipeline project.
-
In CodePipeline, watch the progress as the deployment. When the deployment completes, you can go to the Urls of the load balancer to view and download the SP and IdP metadata.
Once deployed, the SAML proxy has several importan URLs that will be used:
https://[YourSamlProxyDns]/idp
- Contains the metadata for the IdP side of the proxy. This is usually shared with your target SP.https://[YourSamlProxyDns]/Saml2/sp
- Contains the metadata for the SP side of your metadata provider. This is usually shared with your source IdP -https://[YourSamlProxyDns]/Saml2/unsolicited?providerId=[ProviderEntityId]&target=[TargetUrl]
- This is the URL used to initiate a request. This is typically used when the SP does not support an SP-initiated request to the IdP, such as AWS.
The SAML Proxy is preconfigured to support federated authentication to AWS (and AppStream). However, you will need to perform a few steps to connect it with your IdP and your AWS Account. The general process is as follows:
- Download the SAML proxy's SP metadata at
/Saml2/sp
and share it with your IdP. It is expected that your IdP will return the required AWS attributes (i.e.RoleName
,RoleSessionName
) - Download the SAML proxy's IdP metadata at
/idp
. In your AWS account, create an IAM SAML Identity Provider and make sure any Roles that will be used have the appropriate trust policy in place, per the documentation. - In a web browser, use the
/Saml2/unsolicited
URL to initiate an authentication request with the SP's entity ID included, i.e.https://[YourSamlProxyDns]/Saml2/unsolicited?providerId=urn:amazon:webservices
Here is a brief description of the authentication flow:
-> /Saml2/unsolicited
-> SAML Proxy SP - redirect with AuthnRequest to source IdP
-> Authenticate at IdP
-> redirect with assertion to /Saml2/acs
-> SAML Proxy IdP generates custom assertion
-> redirect with SAML Proxy IdP assertion to AWS SP
The project consists of 3 major sets of components:
Any SATOSA-related code and configuration files located in the satosa/
folder:
-
satosa/frontends - Custom frontend plugins. The
saml2_custom.py
file contains a customized frontend that can handle unsolicited requests and custom RelayState parameters. -
satosa/micro_services - Custom microservices that can be used to process and customize assertions.
The
custom_nameid.py
file is a custom microservice that checks ifNameId
(subject) is inUserPrincipalName
format and can convert it toSamAccountName
format. It can also convert the domain to uppercase.The
processors/regex_sub_processor.py
file is a custom attribute processor that can use regex patterns to substitute strings in attribute values. In this project, the processor is used to replace the name of the SAML Identity Provider in theRoleEntitlement
attribute values. -
satosa/config - Configuration files for SATOSA and its plugins. Additional details about files within this directory are below:
-
proxy_conf.yaml - The main parameters that SATOSA uses for configuration. This includes specifying the backend, frontend, and microservice configurations that are used, as well as logging and attribute mappings.
-
internal_attributes.yaml - Maps SAML attributes to names that will are used internally by SATOSA. This can also be used to map an Attribute returned by an IdP to the NameID when passing an assertion to the target SP. The SATOSA docs have more details on this.
-
attributemaps - Contains the attribute mappings that are used to translate the SAML attributes to "internal" attribtues used by Satosa. The
custom.py
file defined AWS-specific attributes likeRoleEntitlement
andRoleSessionName
. -
plugins/backends/saml2_backend.yaml - The configuration for the SAML Proxy's SP. Any target IdP's metadata is defined here. Your IdP's metadata Url is automatically added to this configuration file through an environment variable specified at runtime.
-
plugins/backends/saml2_frontend.yaml - The configuration for the SAML Proxy's IdP. The configuration specifies the custom front plugin (
satosa.frontends.custom.saml2_custom.SAMLUnsolicitedFrontend
). For unsolicited requests, any RelayState endpoints that should be allowed are defined in theallowed_relay_state_urls
parameter. Any SP's metadata is also defined here. The AWS SP metadata Url is included here by default. -
plugins/microservices/custom_nameid.yaml - The settings used by
custom_nameid.py
microservice (seesatosa/micro_services
above for more details). The microservice's class is specified in this file (satosa.micro_services.custom.custom_nameid.ConvertUpnToSamAccountFormat
) anddomain_to_upper
is set totrue
by default in the config to convert the domain name to uppercase. -
plugins/microservices/attribute_processor.yaml - The settings used by
regex_sub_processor.py
microservice (seesatosa/micro_services
above for more details).
-
CloudFormation templates are located in the cloudformation
directory. Below are the templates, listed in order of deployment:
-
cfn-pipeline: Main template used to deploy the base CodePipeline project, code repository with code, ECR repo, and secrets. Also takes in SAML Proxy settings that will be used throughout the deployment.
-
cfn-vpc: Creates the VPC, subnets, and NAT gateways. This is deployed by CodePipeline in the Deploy stage.
-
cfn-ecs-fargate-cluster: Deploys the ECS cluster, load balancer + listener and target groups, ECS service, and task definition. It also creates an S3 bucket to store the load balancer access logs. This is deployed by CodePipeline.
The buildspec files contain scripted operations needed when restoring the site from backup files. More details are below:
-
buildspec-buildimage: Builds the docker image using
docker/Dockerfile
. The Dockerfile pulls the latest SATOSA image from Docker Hub, adds the files insatosa/config
to/etc/satosa/config
and modifies toCMD
to rundocker/write_metadata_key.sh
at container startup. This script will write the PRIVATE_KEY and METADATA_CERTIFICATE envrionment variables to files prior to starting the SATOSA server. -
buildspec-generatekeys: Checks if the SecretsManager secrets that store the metadata private key and certificate contain values and, if not, generates a private key and certificate and stores them in the Secrets Manager Secrets. If the secrets already contain values, then this buildpec does nothing.
-
There is also a CodeBuild project and Buildspec built into the CodePipeline template named
GenerateClusterParameters
. This Buildspec is used to prepare a JSON file containing the parameter names and values that will be used as input to thecfn-ecs-fargate-cluster
template. Though these paramters can be passed into CodePipeline CloudFormation Actions dynamically usingParameterOverrides
, there is a 1000 character limit and so this project works around that. Also, I couldn't find a way to useFn::GetParam
to pass multiple input parameters into a single Parameter (i.e. ClusterSubnets). Because it's tightly coupled to the Pipeline, I opted to write this buildspec into the pipeline template itself, but might move it into thebuildspecs
folder later.
During deployment, the code package uploaded to S3 is extracted into an AWS CodeCommit repository. You can connect to this repository and make changes to the files above, then commit to the master branch.
Anytime a commit to the repo occurs, it will automatically trigger CodePipeline project to apply any changes. This includes automatically rebuilding the container image, updating any CloudFormation stacks, and updating the ECS Service and Task Definition.
The exception to this is if you want to make changes to the pipeline itself, via the cfn-pipeline.yml template used initially to deploy the initial stack. Any changes to this will require manually updating the stack.
ℹ️ Important |
---|
If modifying the CloudFormation templates, remember that the behavior/rules of CloudFormation still apply. If extensive customization is required, it is recommended that you setup a copy of this project for dev/test. |
Delete the stacks in the order listed below. Deleting cfn-pipeline.yml will remove the role used to deploy the other stacks. |
---|
Most of this solution can be uninstalled by deleting the associated CloudFormation stacks.
- If it exists, delete the
[StackName]-fargate
stack. - If it exists, delete the
[StackName]-vpc
stack. - Delete the stack that was created with
cfn-pipeline.yml
Note that there are few artifacts that will cause DELETE_FAILED
on the stacks. They will be left behind and must be cleaned up manually:
- The ECR repository (named
[StackName]-image
). - The S3 bucket and code.zip file that was uploaded to it.
- The artifacts S3 bucket created by the pipeline (
[StackName]-artifactbucket-randomstring
). - The load balancer logs S3 bucket created in the fargate stack (
[StackName]-fargate-loadbalancerlogsbucket-randomstring
)
If time were infinite:
- Enhance the deployment of updated container images - Blue/Green deployment or something similar.
- Load testing and scale out - I didn't have time to test autoscaling. The load balancer is configured with sticky sessions.
- PR to put customizations into SATOSA so they don't need to be injected into the container image.
- ?
- SATOSA on Github: https://github.com/IdentityPython/SATOSA
- SATOSA container on Docker Hub: https://hub.docker.com/r/satosa/satosa
- SATOSA microservice development: https://github.com/IdentityPython/SATOSA/wiki/Anatomy-of-a-response-micro-service
- pysaml2 docs: https://pysaml2.readthedocs.io/en/latest/
- ECS CloudFormation Template: https://github.com/awslabs/aws-cloudformation-templates/tree/master/aws/services/ECS