<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Perfsys]]></title><description><![CDATA[Perfsys]]></description><link>https://perfsys.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 22 Sep 2026 08:16:27 GMT</lastBuildDate><atom:link href="https://perfsys.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to Add AWS Account SAML Login to Keycloak]]></title><description><![CDATA[TL;DR: Import a ready-made Keycloak client JSON for the new AWS account, swap in one account label, and verify three settings that commonly get missed (Name ID format, the SSO URL name, and a rogue ro]]></description><link>https://perfsys.hashnode.dev/how-to-add-aws-account-saml-login-to-keycloak</link><guid isPermaLink="true">https://perfsys.hashnode.dev/how-to-add-aws-account-saml-login-to-keycloak</guid><category><![CDATA[keycloak]]></category><category><![CDATA[AWS]]></category><category><![CDATA[SAML]]></category><category><![CDATA[IAM]]></category><dc:creator><![CDATA[Eugene Orlovsky]]></dc:creator><pubDate>Fri, 18 Sep 2026 10:43:58 GMT</pubDate><content:encoded><![CDATA[<p><strong>TL;DR:</strong> Import a ready-made Keycloak client JSON for the new AWS account, swap in one account label, and verify three settings that commonly get missed (Name ID format, the SSO URL name, and a rogue <code>role_list</code> client scope). Then encode the AWS account ID and IAM role into a two-part ARN string as a Keycloak role, and assign it to the users or groups who need access. Result: one login, every authorized AWS account, no shared passwords.</p>
<p>If your team manages more than one AWS account, you already know the pain of juggling separate logins for each one. Single Sign-On (SSO) through Keycloak solves this. Your team logs in once and gets access to every AWS account they're authorized for, without a separate password for each.</p>
<p>This guide sets up SSO for one new AWS account. You don't build the configuration from scratch — you import a ready-made client JSON, replace one placeholder, then create a role and assign it to whoever needs access.</p>
<p>For a sense of what this looks like at production scale, <a href="https://perfsys.com/case-studies/keycloak-sso-aws-energy-customer-platform/">this case study on a multi-brand Keycloak SSO rollout</a> covers a similar setup built for an energy sector customer platform.</p>
<p>If you'd rather hand the whole thing off, <a href="https://perfsys.com/solutions/keycloak-sso/">Perfsys's Keycloak SSO service</a> covers the full setup, deployment, custom auth flows, MFA, and the runbooks your team needs to operate it afterward.</p>
<p>Throughout, the examples use a fictional account labelled <code>ExampleCorp_Staging</code> with AWS account ID <code>123456789012</code>.</p>
<h2>Step 1: Import the Client</h2>
<p>This is the client JSON. Everything in it is already correct except one placeholder.</p>
<pre><code class="language-json">{
  "clientId": "Amazon_Perfsys_SSO_NAME",
  "protocol": "saml",
  "enabled": true,
  "baseUrl": "/realms/{realm}/protocol/saml/clients/amazon-aws-NAME",
  "redirectUris": ["https://signin.aws.amazon.com/saml"],
  "alwaysDisplayInConsole": true,
  "fullScopeAllowed": false,
  "attributes": {
    "saml_name_id_format": "transient",
    "saml_idp_initiated_sso_url_name": "amazon-aws-NAME",
    "saml_assertion_consumer_url_post": "https://signin.aws.amazon.com/saml",
    "saml.force.post.binding": "true",
    "saml.authnstatement": "true",
    "saml.assertion.signature": "true",
    "saml.server.signature": "true",
    "saml.client.signature": "false",
    "saml.encrypt": "false"
  },
  "protocolMappers": [
    {
      "name": "Session Name",
      "protocol": "saml",
      "protocolMapper": "saml-user-property-mapper",
      "config": {
        "user.attribute": "username",
        "attribute.name": "https://aws.amazon.com/SAML/Attributes/RoleSessionName",
        "attribute.nameformat": "Basic"
      }
    },
    {
      "name": "Session Duration",
      "protocol": "saml",
      "protocolMapper": "saml-hardcode-attribute-mapper",
      "config": {
        "attribute.value": "28800",
        "attribute.name": "https://aws.amazon.com/SAML/Attributes/SessionDuration",
        "attribute.nameformat": "Basic"
      }
    },
    {
      "name": "Session Role",
      "protocol": "saml",
      "protocolMapper": "saml-role-list-mapper",
      "config": {
        "single": "true",
        "attribute.name": "https://aws.amazon.com/SAML/Attributes/Role",
        "attribute.nameformat": "Basic"
      }
    }
  ],
  "defaultClientScopes": [],
  "optionalClientScopes": []
}
</code></pre>
<h3>Fields to update</h3>
<p>Replace <code>NAME</code> with a short label for the account, and <code>{realm}</code> with your realm. Nothing else changes per account — the AWS account ID does not appear here at all, only in Step 2.</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Replace with</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><code>clientId</code></td>
<td><code>Amazon_Perfsys_SSO_</code> + your label</td>
<td><code>Amazon_Perfsys_SSO_ExampleCorp_Staging</code></td>
</tr>
<tr>
<td><code>saml_idp_initiated_sso_url_name</code></td>
<td><code>amazon-aws-</code> + your label, <strong>underscores turned into hyphens</strong></td>
<td><code>amazon-aws-ExampleCorp-Staging</code></td>
</tr>
<tr>
<td><code>baseUrl</code></td>
<td>the realm name, and the same SSO URL name as above</td>
<td><code>/realms/perfsys/protocol/saml/clients/amazon-aws-ExampleCorp-Staging</code></td>
</tr>
</tbody></table>
<p>Two traps in that table. Underscores are not allowed in the SSO URL name, so <code>ExampleCorp_Staging</code> becomes <code>ExampleCorp-Staging</code>. And <code>baseUrl</code> repeats the SSO URL name — Keycloak keeps no link between the two, so if they disagree the client still works via its real URL while the account-console tile points at a dead path.</p>
<h3>Import it</h3>
<ol>
<li><p>Keycloak admin console → your realm → <strong>Clients</strong> → <strong>Import client</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/685a4a6d4bd715b9925d0076/b66db0f7-db1e-4ec8-a3b2-a66ca7e45c44.jpg" alt="" style="display:block;margin:0 auto" />
</li>
<li><p><strong>Browse</strong>, pick your file, then <strong>Save</strong>. Keycloak previews the JSON and fills in the Client ID and Type from it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/685a4a6d4bd715b9925d0076/ca7eee89-b454-4bdd-a9d7-dcf9a7fb9c93.jpg" alt="" style="display:block;margin:0 auto" /></li>
</ol>
<blockquote>
<p>[!NOTE] This screen shows <strong>Encrypt assertions: On</strong>, but the form renders that toggle from its own defaults rather than from your file. <code>saml.encrypt</code> is <code>false</code> in the JSON, and <code>false</code> is what gets saved.</p>
</blockquote>
<h3>Verify three things</h3>
<p>The import does the work, but confirm it landed. All three are on the client's <strong>Settings</strong> tab except the last.</p>
<p><strong>Name ID format is</strong> <code>transient</code><strong>.</strong> Under SAML capabilities.</p>
<img src="https://cdn.hashnode.com/uploads/covers/685a4a6d4bd715b9925d0076/c563aeb9-d6ca-4f9e-9f9a-851170198aa8.jpg" alt="" style="display:block;margin:0 auto" />

<p><strong>The SSO URL name and redirect URI are right.</strong> Under Access settings. Also confirm <strong>Always display in UI</strong> is on, at the top of the same tab — without it the client never appears at <code>/realms/{realm}/account/applications</code>, and the user you just granted access to reports they cannot see the account.</p>
<img src="https://cdn.hashnode.com/uploads/covers/685a4a6d4bd715b9925d0076/12c54641-f53c-4448-a016-1252e95994d3.png" alt="" style="display:block;margin:0 auto" />

<p><strong>The</strong> <code>role_list</code> <strong>client scope is gone.</strong> Open the <strong>Client scopes</strong> tab. It should list only <code>&lt;client&gt;-dedicated</code>. The JSON sets <code>defaultClientScopes: []</code>, but Keycloak attaches <code>role_list</code> to SAML clients anyway, so check rather than assume. If it is there, click <strong>Remove</strong> on that row.</p>
<img src="https://cdn.hashnode.com/uploads/covers/685a4a6d4bd715b9925d0076/bf08dc1e-1e63-4445-b740-fc179660d5c4.png" alt="" style="display:block;margin:0 auto" />

<p><code>role_list</code> is worth understanding, because it produces a failure that points nowhere near Keycloak. The scope carries its own role mapper emitting the attribute <code>Role</code>. Your client's <code>Session Role</code> mapper emits <code>https://aws.amazon.com/SAML/Attributes/Role</code>, which is the name AWS looks for. Keycloak runs exactly one role mapper per login and keeps the first it finds, so which one wins is luck. When <code>role_list</code> wins, AWS finds no role attribute and answers <em>Your request included an invalid SAML response</em> — the same message it uses for a bad signature.</p>
<blockquote>
<p>[!WARNING] Remove the scope from the client. Never edit the mapper inside the <code>role_list</code> scope, because that scope is shared with every other SAML client in the realm.</p>
</blockquote>
<h2>Step 2: Create the Role</h2>
<p>The role name is where the AWS account ID finally appears. One Keycloak role encodes two ARNs, comma-separated, no space.</p>
<ol>
<li><p>Open the client → <strong>Roles</strong> tab → <strong>Create role</strong>.</p>
</li>
<li><p>Enter the role name:</p>
</li>
</ol>
<pre><code class="language-plaintext">arn:aws:iam::{ACCOUNT_ID}:role/{ROLE_NAME},arn:aws:iam::{ACCOUNT_ID}:saml-provider/SAML-Perfsys
</code></pre>
<p>For the example account that gives:</p>
<pre><code class="language-plaintext">arn:aws:iam::123456789012:role/Keycloak-AdministratorAccess,arn:aws:iam::123456789012:saml-provider/SAML-Perfsys
</code></pre>
<ol>
<li>Add a description naming the account, then <strong>Save</strong>.</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/685a4a6d4bd715b9925d0076/9b60765c-eb6e-4f78-b15e-36d2a624576e.png" alt="" style="display:block;margin:0 auto" />

<p><em>The whole string is a single role name. The comma joining the two ARNs takes no space after it. This is the field people get wrong most often.</em></p>
<img src="https://cdn.hashnode.com/uploads/covers/685a4a6d4bd715b9925d0076/89e347a5-bbef-4022-8442-5c46546ac24f.png" alt="" style="display:block;margin:0 auto" />

<p><code>{ROLE_NAME}</code> is the IAM role in the target account: <code>Keycloak-AdministratorAccess</code> for admin, <code>Federated-ReadOnly</code> for read-only. <code>SAML-Perfsys</code> is the IAM SAML provider — do not derive it from your client name. The CloudFormation stack hardcodes it, so it is <code>SAML-Perfsys</code> in every account regardless of what you called the client.</p>
<p>Both privilege levels go on the <strong>same</strong> client. A second access level does not need a second Keycloak client. <code>Session Role</code> has <code>single: "true"</code>, so every role the user holds becomes another value of one <code>Role</code> attribute — exactly what AWS needs to render its role picker at login. One client per AWS account, one role per privilege level.</p>
<h2>Step 3: Assign the Role</h2>
<ol>
<li><p><strong>Users</strong> → the person → <strong>Role mapping</strong> tab → <strong>Assign role</strong>.</p>
</li>
<li><p>Change the filter dropdown from <strong>Filter by realm roles</strong> to <strong>Filter by clients</strong>.</p>
</li>
<li><p>Search the account ID or client name, tick the role, <strong>Assign</strong>.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/685a4a6d4bd715b9925d0076/b9798bad-f262-4557-8ed8-a32a7ee73005.png" alt="" style="display:block;margin:0 auto" />

<p><em>Step 2 is not optional. Client roles are invisible in the default realm-roles view, so skipping the dropdown shows an empty list and looks like the role was never created. The client badge on the left is also how you tell two accounts apart when the ARNs are truncated.</em></p>
<p>For a whole team, use <strong>Groups</strong> → the group → <strong>Role mapping</strong>, same filter change. Check the membership first: every member gets that AWS access.</p>
<p>Test it:</p>
<pre><code class="language-plaintext">https://{keycloak-host}/realms/{realm}/protocol/saml/clients/amazon-aws-ExampleCorp-Staging
</code></pre>
<h2>Troubleshooting</h2>
<p>The two AWS error strings rule out different halves of the system. Read which one you got before investigating.</p>
<p><strong>"Your request included an invalid SAML response"</strong> — AWS could not validate the assertion, or found no role attribute in it. Check the <code>role_list</code> scope first; it is the cheap cause and takes one look at the Client scopes tab. Only then look at the IAM SAML provider in the target account.</p>
<p><strong>"Not authorized to perform sts:AssumeRoleWithSAML"</strong> — the assertion validated, then failed at the trust boundary. A signature problem would have given the other error, so Keycloak is almost certainly fine. Look at the role's trust policy, the account ID, and <code>MaxSessionDuration</code> against the 28800 the Session Duration mapper requests.</p>
<p><strong>Only one role in the picker</strong> is usually not a bug. A role must be <em>assigned</em> to the user or their group, not merely defined on the client. Defining <code>Federated-ReadOnly</code> does not put it in anyone's assertion.</p>
<h2>Why This Matters for Your Team</h2>
<p>Every field ties back to something concrete. The Client ID and SSO URL name identify which AWS account you're connecting to. The Name ID format controls how Keycloak identifies the user to AWS on each login. The role's ARN string is what actually grants access, and it has to match the target account's IAM setup exactly, account ID and role name included.</p>
<p>Get these three steps right and your team gets centralized access to every AWS account they manage. No shared passwords, no separate logins to track. When someone leaves, offboarding is a single step instead of a checklist you hope nobody forgets.</p>
]]></content:encoded></item><item><title><![CDATA[Perfsys]]></title><description><![CDATA[Perfsys is your DevOps and AWS partner for launching MVPs, supporting production workloads, and optimizing cloud infrastructure. We work with small and medium businesses to implement CI/CD, automate deployments, and reduce AWS costs without sacrifici...]]></description><link>https://perfsys.hashnode.dev/perfsys</link><guid isPermaLink="true">https://perfsys.hashnode.dev/perfsys</guid><dc:creator><![CDATA[Eugene Orlovsky]]></dc:creator><pubDate>Tue, 24 Jun 2025 06:53:38 GMT</pubDate><content:encoded><![CDATA[<p>Perfsys is your DevOps and AWS partner for launching MVPs, supporting production workloads, and optimizing cloud infrastructure. We work with small and medium businesses to implement CI/CD, automate deployments, and reduce AWS costs without sacrificing performance. With our cloud-native expertise, you get a stable foundation for growth. Discover more at Perfsys.</p>
<p><a target="_blank" href="https://perfsys.com/">https://perfsys.com/</a></p>
]]></content:encoded></item></channel></rss>