|
| 1 | +Description: Keycloak Server |
| 2 | + |
| 3 | +Metadata: |
| 4 | + AWS::CloudFormation::Interface: |
| 5 | + ParameterGroups: |
| 6 | + - Label: |
| 7 | + default: Keycloak Configuration |
| 8 | + Parameters: |
| 9 | + - Keypair |
| 10 | + - ServiceAccountPasswordSecretArn |
| 11 | + - VpcId |
| 12 | + - PublicSubnet |
| 13 | + - ServiceAccountUserDN |
| 14 | + - UsersDN |
| 15 | + - LDAPConnectionURI |
| 16 | + - CogntioUserPoolId |
| 17 | + - EnvironmentBaseURL |
| 18 | + - SAMLRedirectUrl |
| 19 | + |
| 20 | +Parameters: |
| 21 | + Keypair: |
| 22 | + Description: EC2 Keypair to access management instance. |
| 23 | + Type: AWS::EC2::KeyPair::KeyName |
| 24 | + |
| 25 | + ServiceAccountPasswordSecretArn: |
| 26 | + Type: String |
| 27 | + AllowedPattern: ^(?:arn:(?:aws|aws-us-gov|aws-cn):secretsmanager:[a-z0-9-]{1,20}:[0-9]{12}:secret:[A-Za-z0-9-_+=,\.@]{1,128})?$ |
| 28 | + Description: Directory Service Root (Service Account) Password Secret ARN |
| 29 | + |
| 30 | + VpcId: |
| 31 | + Type: AWS::EC2::VPC::Id |
| 32 | + AllowedPattern: vpc-[0-9a-f]{17} |
| 33 | + ConstraintDescription: VpcId must begin with 'vpc-', only contain letters (a-f) or numbers(0-9) and must be 21 characters in length |
| 34 | + |
| 35 | + PublicSubnet: |
| 36 | + Type: AWS::EC2::Subnet::Id |
| 37 | + AllowedPattern: subnet-.+ |
| 38 | + Description: Select a public subnet from the already selected VPC |
| 39 | + |
| 40 | + ServiceAccountUserDN: |
| 41 | + Type: String |
| 42 | + AllowedPattern: .+ |
| 43 | + Description: Provide the Distinguished name (DN) of the service account user in the Active Directory |
| 44 | + |
| 45 | + UsersDN: |
| 46 | + Type: String |
| 47 | + AllowedPattern: .+ |
| 48 | + Description: Please provide Users Organization Unit in your active directory under which all of your users exist. For example, OU=Users,DC=RES,DC=example,DC=internal |
| 49 | + |
| 50 | + LDAPConnectionURI: |
| 51 | + Type: String |
| 52 | + AllowedPattern: .+ |
| 53 | + Description: Please provide the active directory connection URI (e.g. ldap://www.example.com) |
| 54 | + |
| 55 | + CogntioUserPoolId: |
| 56 | + Type: String |
| 57 | + AllowedPattern: .+ |
| 58 | + Description: Please provide the Cognito user pool id (e.g. us-east-1_ababab) |
| 59 | + |
| 60 | + EnvironmentBaseURL: |
| 61 | + Type: String |
| 62 | + AllowedPattern: https?://.+ |
| 63 | + Description: Please provide your base URL for your environment |
| 64 | + |
| 65 | + SAMLRedirectUrl: |
| 66 | + Type: String |
| 67 | + AllowedPattern: https://.+\.amazoncognito\.com/saml2/idpresponse |
| 68 | + Description: Please provide the SAML redirect URL |
| 69 | + |
| 70 | +Mappings: |
| 71 | + Keycloak: |
| 72 | + Config: |
| 73 | + Version: "24.0.3" |
| 74 | + |
| 75 | + |
| 76 | +Resources: |
| 77 | + KeycloakSecret: |
| 78 | + Type: AWS::SecretsManager::Secret |
| 79 | + Properties: |
| 80 | + Name: !Sub |
| 81 | + - KeycloakSecret-${AWS::StackName}-${StackIdSuffix} |
| 82 | + - StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] |
| 83 | + Description: Keycloak secret |
| 84 | + GenerateSecretString: |
| 85 | + PasswordLength: 14 |
| 86 | + ExcludePunctuation: true |
| 87 | + |
| 88 | + KeycloakEC2InstanceRole: |
| 89 | + Type: AWS::IAM::Role |
| 90 | + Properties: |
| 91 | + AssumeRolePolicyDocument: |
| 92 | + Version: 2012-10-17 |
| 93 | + Statement: |
| 94 | + - Effect: Allow |
| 95 | + Principal: |
| 96 | + Service: |
| 97 | + - ec2.amazonaws.com |
| 98 | + Action: |
| 99 | + - sts:AssumeRole |
| 100 | + ManagedPolicyArns: |
| 101 | + - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore |
| 102 | + Policies: |
| 103 | + - PolicyName: KeycloakEC2InstancePolicy |
| 104 | + PolicyDocument: |
| 105 | + Version: 2012-10-17 |
| 106 | + Statement: |
| 107 | + - Effect: Allow |
| 108 | + Action: secretsmanager:GetSecretValue |
| 109 | + Resource: |
| 110 | + - !Ref KeycloakSecret |
| 111 | + - !Ref ServiceAccountPasswordSecretArn |
| 112 | + - Effect: Allow |
| 113 | + Action: |
| 114 | + - logs:CreateLogGroup |
| 115 | + - logs:CreateLogStream |
| 116 | + - logs:PutLogEvents |
| 117 | + Resource: '*' |
| 118 | + |
| 119 | + KeycloakEC2InstanceProfile: |
| 120 | + Type: AWS::IAM::InstanceProfile |
| 121 | + Properties: |
| 122 | + Roles: |
| 123 | + - !Ref KeycloakEC2InstanceRole |
| 124 | + |
| 125 | + KeycloakSecurityGroup: |
| 126 | + Type: AWS::EC2::SecurityGroup |
| 127 | + Properties: |
| 128 | + GroupDescription: Keycloak security group |
| 129 | + VpcId: !Ref VpcId |
| 130 | + SecurityGroupIngress: |
| 131 | + - IpProtocol: tcp |
| 132 | + FromPort: 80 |
| 133 | + ToPort: 80 |
| 134 | + CidrIp: "0.0.0.0/0" |
| 135 | + |
| 136 | + KeycloakEC2Instance: |
| 137 | + Type: AWS::EC2::Instance |
| 138 | + DependsOn: |
| 139 | + - KeycloakSecurityGroup |
| 140 | + - KeycloakEC2InstanceProfile |
| 141 | + - KeycloakSecret |
| 142 | + CreationPolicy: |
| 143 | + ResourceSignal: |
| 144 | + Timeout: PT15M |
| 145 | + Properties: |
| 146 | + ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64:75}}' |
| 147 | + InstanceType: t3.micro |
| 148 | + KeyName: !Ref Keypair |
| 149 | + IamInstanceProfile: !Ref KeycloakEC2InstanceProfile |
| 150 | + SubnetId: !Ref PublicSubnet |
| 151 | + SecurityGroupIds: |
| 152 | + - !Ref KeycloakSecurityGroup |
| 153 | + Tags: |
| 154 | + - Key: Name |
| 155 | + Value: !Sub keycloak-${AWS::StackName} |
| 156 | + UserData: |
| 157 | + Fn::Base64: |
| 158 | + Fn::Sub: |
| 159 | + - | |
| 160 | + #!/bin/sh -x |
| 161 | + mkdir -p /root/bootstrap && cd /root/bootstrap |
| 162 | + mkdir -p /root/bootstrap/logs/ |
| 163 | + exec > /root/bootstrap/logs/userdata.log 2>&1 |
| 164 | +
|
| 165 | + #Install java17 |
| 166 | + sudo yum -y install java-17-amazon-corretto-headless |
| 167 | + export KEYCLOAK_VERSION=${KeycloakVersion} |
| 168 | + wget https://github.com/keycloak/keycloak/releases/download/$KEYCLOAK_VERSION/keycloak-$KEYCLOAK_VERSION.zip |
| 169 | + unzip keycloak-$KEYCLOAK_VERSION.zip |
| 170 | + |
| 171 | + cd keycloak-$KEYCLOAK_VERSION |
| 172 | + |
| 173 | + export KC_HTTP_PORT=80 |
| 174 | + export KEYCLOAK_ADMIN=admin |
| 175 | + set +x |
| 176 | + export KEYCLOAK_ADMIN_PASSWORD=$(aws secretsmanager get-secret-value --secret-id ${KeycloakSecret} --query SecretString --region ${AWS::Region} --output text) |
| 177 | + set -x |
| 178 | +
|
| 179 | + # Start Keycloak |
| 180 | + sudo -E nohup ./bin/kc.sh start-dev --http-port 80 > keycloak.log & |
| 181 | + sleep 30 |
| 182 | +
|
| 183 | + SERVER_URL="http://0.0.0.0:80" |
| 184 | + MAX_ATTEMPTS=15 |
| 185 | + RETRY_INTERVAL=10 |
| 186 | +
|
| 187 | + attempt=0 |
| 188 | + while [ $attempt -lt $MAX_ATTEMPTS ]; do |
| 189 | + response=$(curl -s -o /dev/null -w "%{http_code}" "$SERVER_URL") |
| 190 | + if [ "$response" == "302" ]; then |
| 191 | + echo "Server is up!" |
| 192 | + break |
| 193 | + else |
| 194 | + echo "Server is not yet up. Retrying in $RETRY_INTERVAL seconds..." |
| 195 | + sleep $RETRY_INTERVAL |
| 196 | + ((attempt++)) |
| 197 | + fi |
| 198 | + done |
| 199 | + |
| 200 | + if [ $(curl -s -o /dev/null -w "%{http_code}" "$SERVER_URL") != 302 ]; then |
| 201 | + /opt/aws/bin/cfn-signal -e 1 --stack "${AWS::StackName}" --resource "KeycloakEC2Instance" --region "${AWS::Region}" |
| 202 | + fi |
| 203 | +
|
| 204 | + echo "Keycloak server is up" |
| 205 | + # Login to Keycloak |
| 206 | + set +x |
| 207 | + ./bin/kcadm.sh config credentials --server $SERVER_URL --realm master --user admin --password $KEYCLOAK_ADMIN_PASSWORD |
| 208 | + set -x |
| 209 | +
|
| 210 | + # Create realm named 'res' |
| 211 | + ./bin/kcadm.sh create realms -s realm=res -s id=res -s enabled=true -o |
| 212 | +
|
| 213 | + # Set sslRequired to NONE |
| 214 | + ./bin/kcadm.sh update realms/master -s sslRequired=NONE --server $SERVER_URL |
| 215 | + ./bin/kcadm.sh update realms/res -s sslRequired=NONE --server $SERVER_URL |
| 216 | + |
| 217 | + #Configure Keycloak |
| 218 | + #Get ServiceAccount passsword |
| 219 | + set +x |
| 220 | + serviceAccountPassword=$(aws secretsmanager get-secret-value --secret-id ${ServiceAccountPasswordSecretArn} --query SecretString --region ${AWS::Region} --output text) |
| 221 | +
|
| 222 | + #Create storage component to sync from AD |
| 223 | + componentId=$(./bin/kcadm.sh create components -s name=ldap -s parentId=res -s providerId=ldap -s providerType=org.keycloak.storage.UserStorageProvider \ |
| 224 | + -s 'config.authType=["simple"]' -s "config.bindCredential=[\"$serviceAccountPassword\"]" -s 'config.bindDn=["${ServiceAccountUserDN}"]' \ |
| 225 | + -s 'config.connectionUrl=["${LDAPConnectionURI}"]' -s 'config.editMode=["READ_ONLY"]' -s 'config.enabled=["true"]' -s 'config.rdnLDAPAttribute=["cn"]' \ |
| 226 | + -s 'config.searchScope=["2"]' -s 'config.usernameLDAPAttribute=["sAMAccountName"]' \ |
| 227 | + -s 'config.usersDn=["${UsersDN}"]' -s 'config.uuidLDAPAttribute=["objectGUID"]' \ |
| 228 | + -s 'config.vendor=["ad"]' -s 'config.userObjectClasses=["person, organizationalPerson, user"]' -r res -i) |
| 229 | + set -x |
| 230 | +
|
| 231 | + # Trigger user sync |
| 232 | + ./bin/kcadm.sh create user-storage/$componentId/sync?action=triggerFullSync -r res |
| 233 | +
|
| 234 | + #Create SSO SAML client for SSO |
| 235 | + clientId=$(./bin/kcadm.sh create clients -r res -s baseUrl=${EnvironmentBaseURL} \ |
| 236 | + -s clientId=urn:amazon:cognito:sp:${CogntioUserPoolId} -s name=saml -s protocol=saml -s 'redirectUris=["*"]' -s rootUrl=${EnvironmentBaseURL} \ |
| 237 | + -s 'attributes.saml_name_id_format=email' -s 'attributes."post.logout.redirect.uris"=${EnvironmentBaseURL}' \ |
| 238 | + -s 'attributes."saml.client.signature"=false' -s 'attributes."saml.force.post.binding"=true' -s 'attributes."saml.authnstatement"=true' \ |
| 239 | + -s 'attributes."saml_assertion_consumer_url_post"=${SAMLRedirectUrl}' \ |
| 240 | + -s 'attributes.saml_single_logout_service_url_redirect=${EnvironmentBaseURL}' -i) |
| 241 | +
|
| 242 | + # Create email mapper |
| 243 | + ./bin/kcadm.sh create clients/$clientId/protocol-mappers/models -s name=email_mapper -s protocol=saml -s protocolMapper=saml-user-property-mapper \ |
| 244 | + -s 'config."attribute.name"=email' -s 'config."attribute.nameformat"=Unspecified' -s 'config."friendly.name"=email_mapper' -s 'config."user.attribute"=email' -r res |
| 245 | +
|
| 246 | + ##Schedule crontabs |
| 247 | + #Install crontab on al3 |
| 248 | + sudo yum -y install cronie |
| 249 | + sudo systemctl enable crond.service |
| 250 | + sudo systemctl start crond.service |
| 251 | +
|
| 252 | + #Crontab1 - service account password rotation - script |
| 253 | + echo -e "#!/bin/sh -x |
| 254 | + exec >> /root/bootstrap/logs/userdata.log 2>&1 |
| 255 | + echo Updating service account password |
| 256 | + cd /root/bootstrap/keycloak-$KEYCLOAK_VERSION |
| 257 | + SERVER_URL=\"http://0.0.0.0:80\" |
| 258 | + set +x |
| 259 | + kc_admin_password=\$(aws secretsmanager get-secret-value --secret-id ${KeycloakSecret} --query SecretString --region ${AWS::Region} --output text) |
| 260 | + serviceAccountPassword=\$(aws secretsmanager get-secret-value --secret-id ${ServiceAccountPasswordSecretArn} --query SecretString --region ${AWS::Region} --output text) |
| 261 | + ./bin/kcadm.sh config credentials --server \$SERVER_URL --realm master --user admin --password \$kc_admin_password |
| 262 | + ./bin/kcadm.sh update components/$componentId -s name=ldap -s parentId=res -s providerId=ldap -s providerType=org.keycloak.storage.UserStorageProvider \\ |
| 263 | + -s 'config.authType=[\"simple\"]' -s \"config.bindCredential=[\\\"\$serviceAccountPassword\\\"]\" -s 'config.bindDn=[\"${ServiceAccountUserDN}\"]' \\ |
| 264 | + -s 'config.connectionUrl=[\"${LDAPConnectionURI}\"]' -s 'config.editMode=[\"READ_ONLY\"]' -s 'config.enabled=[\"true\"]' -s 'config.rdnLDAPAttribute=[\"cn\"]' \\ |
| 265 | + -s 'config.searchScope=[\"2\"]' -s 'config.usernameLDAPAttribute=[\"sAMAccountName\"]' \\ |
| 266 | + -s 'config.usersDn=[\"${UsersDN}\"]' -s 'config.uuidLDAPAttribute=[\"objectGUID\"]' \\ |
| 267 | + -s 'config.vendor=[\"ad\"]' -s 'config.userObjectClasses=[\"person, organizationalPerson, user\"]' -r res |
| 268 | + set -x |
| 269 | + ./bin/kcadm.sh create user-storage/$componentId/sync?action=triggerFullSync -r res |
| 270 | + " > /root/bootstrap/password_rotation.sh |
| 271 | + chmod +x /root/bootstrap/password_rotation.sh |
| 272 | +
|
| 273 | + #Crontab2 - user sync - script |
| 274 | + echo -e "#!/bin/sh -x |
| 275 | + exec >> /root/bootstrap/logs/userdata.log 2>&1 |
| 276 | + echo Syncing users |
| 277 | + cd /root/bootstrap/keycloak-$KEYCLOAK_VERSION |
| 278 | + SERVER_URL=\"http://0.0.0.0:80\" |
| 279 | + set +x |
| 280 | + kc_admin_password=\$(aws secretsmanager get-secret-value --secret-id ${KeycloakSecret} --query SecretString --region ${AWS::Region} --output text) |
| 281 | + ./bin/kcadm.sh config credentials --server \$SERVER_URL --realm master --user admin --password \$kc_admin_password |
| 282 | + set -x |
| 283 | + ./bin/kcadm.sh create user-storage/$componentId/sync?action=triggerFullSync -r res |
| 284 | + " > /root/bootstrap/user_sync.sh |
| 285 | + chmod +x /root/bootstrap/user_sync.sh |
| 286 | +
|
| 287 | + (crontab -l; echo "*/30 * * * * /root/bootstrap/password_rotation.sh") | crontab - |
| 288 | + (crontab -l; echo "*/5 * * * * /root/bootstrap/user_sync.sh") | crontab - |
| 289 | +
|
| 290 | + # Signal stack to continue based on last command output |
| 291 | + /opt/aws/bin/cfn-signal -e $? --stack "${AWS::StackName}" --resource "KeycloakEC2Instance" --region "${AWS::Region}" |
| 292 | + - KeycloakVersion: !FindInMap [Keycloak, Config, Version] |
| 293 | + |
| 294 | +Outputs: |
| 295 | + KeycloakUrl: |
| 296 | + Description: Keycloak administrator URL |
| 297 | + Value: !Sub http://${KeycloakEC2Instance.PublicIp}:80 |
| 298 | + KeycloakAdminPasswordSecretArn: |
| 299 | + Description: Keycloak password for admin user |
| 300 | + Value: !Sub ${KeycloakSecret} |
0 commit comments