<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Francis Augusto Medeiros-Logeay</title>
    <description>IT Engineer with background in IT-Law, running enthusiast</description>
    <link>https://francisaugusto.com/</link>
    <atom:link href="https://francisaugusto.com/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Sun, 30 Nov 2025 21:02:15 +0100</pubDate>
    <lastBuildDate>Sun, 30 Nov 2025 21:02:15 +0100</lastBuildDate>
    <generator>Jekyll v3.9.2</generator>
    
      <item>
        <title>Platform Single Sign-on DIY</title>
        <description>&lt;h3 id=&quot;what&quot;&gt;What?&lt;/h3&gt;

&lt;p&gt;This is a post about how to implement &lt;a href=&quot;https://support.apple.com/en-vn/guide/deployment/dep7bbb05313/web&quot;&gt;Platform Single Sign-on&lt;/a&gt;, Apple’s framework for simplifying logins from macOS devices. It builds on the &lt;a href=&quot;https://support.apple.com/en-vn/guide/deployment/depfdbf18f55/web&quot;&gt;SSO Extensions&lt;/a&gt;, but it takes it a bit further. It is also a collection of thoughts about Platform Single Sign-on and the challenges when designing it.&lt;/p&gt;

&lt;p&gt;Why, you ask? The reason is pretty simple: it is almost impossible to find good documentation where we can understand clearly what is it that Apple want IdPs to implement when they develop their SSO Extensions. The only exception to my impression on this is the &lt;a href=&quot;https://twocanoes.com/psso-technical-deep-dive/&quot;&gt;excellent article&lt;/a&gt; written by Timothy Perfitt from &lt;a href=&quot;https://twocanoes.com&quot;&gt;Twocanoes&lt;/a&gt; on the subject. Timothy also wrote a very popular example on &lt;a href=&quot;https://github.com/twocanoes/psso-server-go/tree/main&quot;&gt;how to implement a simple Platform SSO server.&lt;/a&gt;. 
I don’t want to repeat what Timothy wrote on his &lt;a href=&quot;https://twocanoes.com/sso/&quot;&gt;series of articles&lt;/a&gt; about Platform SSO. I’d rather go a bit further and discuss ideas and design possibilities, as well as what I consider lacking.&lt;/p&gt;

&lt;h3 id=&quot;disclaimers&quot;&gt;Disclaimers&lt;/h3&gt;

&lt;p&gt;My opinions are mine and mine only, and do not by any means reflect those of my employer.&lt;/p&gt;

&lt;p&gt;Everything written here is based on using shared keys from the Secure Enclave as the Authentication Method for the Platform SSO. Other authentication methods, such as using Passwords or smartcards are not covered.&lt;/p&gt;

&lt;h3 id=&quot;what-is-platform-sso&quot;&gt;What is Platform SSO?&lt;/h3&gt;

&lt;p&gt;Platform Single Sign-On is a macOS feature allowing Macs to seamless authenticate to an IdP when they authenticate locally on their Macs. This avoids double authentication, that is, authenticating on the Mac &lt;em&gt;and&lt;/em&gt; authenticating to the IdP using the same or different credentials.&lt;/p&gt;

&lt;h3 id=&quot;implementing-platform-sso-from-the-perspective-of-an-idp&quot;&gt;Implementing Platform SSO from the perspective of an IdP&lt;/h3&gt;

&lt;p&gt;The rumour goes that Platform SSO hasn’t really become popular. The only two known implementations took a few years to became available, and those are basically Microsoft’s and Okta’s. It is difficult to speculate why this happened, but I have a few theories:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Lack of MDM native support&lt;/strong&gt;: Platform SSO (PSSO from now on) is basically IdP-centric. Besides configuring Platform SSO and having the possibility to integrate device registration with MDM’s, its implementation requires IdP-compatibility and tight cooperation between Mac admins and teams responsible for authentication/identity management;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Substantial implementation of API’s on IdPs&lt;/strong&gt;: PSSO requires some APIs that need to be implemented on IdPs.  This basically requires that every IdP has to come up with their own implementation.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Scarce documentation and examples&lt;/strong&gt;: This is probably debatable. There &lt;em&gt;is&lt;/em&gt; &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/authentication-process&quot;&gt;documentation on how to implement PSSO&lt;/a&gt;, but there is little documentation with code examples and possible pattern flows. Or, in other words, sometimes it is hard to understand what Apple is thinking or how they want IdPs to implement this.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Passkeys&lt;/strong&gt;: One could simply ask: why go through this hassle if the IdP could simply support Passkeys and call it a day? While Platform SSO gives macOS users the best possible experience, as well as giving IdP admins good tools to manage sessions, Passkeys are almost as easy to use, without having to implement a whole set of APIs to support just macOS devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nevertheless, PSSO is a great addition to any IdP who wants to offer an unbeatable user experience for macOS users. 
The organization I work for has (through me :)) developed a &lt;a href=&quot;https://github.com/unioslo/keycloak-psso-extension&quot;&gt;Platform Single Sign-on extension&lt;/a&gt; for &lt;a href=&quot;https://www.keycloak.org&quot;&gt;Keycloak&lt;/a&gt;, an open-source IdP and IAM that is quite popular. Keycloak is incredibly extendable, and could easily be extended to support PSSO.&lt;/p&gt;

&lt;h3 id=&quot;requirements-for-idps-and-how-we-did-it&quot;&gt;Requirements for IdPs and how we did it&lt;/h3&gt;

&lt;p&gt;Before we dive into what IdPs need to offer Platform SSO, it is important to distinguish an SSO Extension to a Platform SSO Extension. Both will be on the same package, but one can develop an SSO Extension without support for Platform SSO. 
What does an SSO Extension does? Well, it basically intercepts any call to a configurable URL (the configuration needs to be managed by an MDM) so that you can add some logic on how to authenticate the user, so that other applications/websites can reuse that authentication.&lt;/p&gt;

&lt;p&gt;On the &lt;a href=&quot;https://twocanoes.com/building-a-single-sign-on-extension-on-macos/&quot;&gt;example provided by Twocanoes&lt;/a&gt;, that logic, for example, is simply saving cookies the IdP sets into the Keychain, so that they are sent back to whatever attempts to authenticate again.  With PSSO we might want to do things a bit differently, but the point is that there’s no recipe for what an SSO Extension should do to authenticate the user - it needs to be implemented according to the logic of the IdP. Cookies are possibly the most common pattern here, so it makes sense to use them in this context.&lt;/p&gt;

&lt;p&gt;On an SSO Extension, everytime an application or Safari hits a predefined endpoint, the OS calles your extension, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beginAuthorization&lt;/code&gt;method  is called. From here on you are free to do whatever you want: present a login screen if the user isn’t authenticated, send back some cookies, etc.&lt;/p&gt;

&lt;p&gt;But Platform SSO takes this further: it fetches tokens from the IdP on behalf of the user so that they can be used by the SSO Extension to authenticate the user.&lt;/p&gt;

&lt;p&gt;Let’s suppose your IdP does a standard OIDC flow.  Platform SSO doesn’t change that, and you can develop your SSO to cope with that OIDC flow. What Platform SSO introduces is the possibility of registering the device and the user on the IdP &lt;em&gt;and&lt;/em&gt; fetching a token automatically whenever the user authenticates on the Mac. Then the SSO Extension can use that token as a &lt;em&gt;credential&lt;/em&gt; for the user,  instead of simply presenting a login screen.  The idea is that when the abovementioned &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beginAuthorization&lt;/code&gt;method is called on your SSO Extension, you inject that credential (or make it available for the IdP as a cookie, for example - I wouldn’t do that, but it is possible) into the request, and your IdP will evaluate it, the same way it evaluates a password, a MFA credential, etc.&lt;/p&gt;

&lt;p&gt;So, what do you need to implement, basically, to provide PSSO on your IdP? Well, here’s the answer, but notice that there are many ways to Rome:&lt;/p&gt;

&lt;p&gt;Custom endpoints (Apple doesn’t really tell you how you create these, and is not so opinionated about it):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;an endpoint to register the device (called &lt;em&gt;Device registration&lt;/em&gt; by Apple)&lt;/li&gt;
  &lt;li&gt;an endpoint to register the user (called &lt;em&gt;User registration&lt;/em&gt;), which is basically to create some sort of credential for the user based on his key - more about keys later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Endpoints that have to conform to Apple’s specifications:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;an endpoint to request a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nonce&lt;/code&gt; value to be used during logins.&lt;/li&gt;
  &lt;li&gt;an endpoint to request tokens, which is basically an endpoint where you send a &lt;em&gt;&lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/creating-and-validating-a-login-request&quot;&gt;login request&lt;/a&gt;&lt;/em&gt; and obtain a &lt;em&gt;&lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/creating-a-json-web-encryption-jwe-login-response&quot;&gt;login response&lt;/a&gt;&lt;/em&gt;.  A &lt;em&gt;login request&lt;/em&gt; and a &lt;em&gt;login response&lt;/em&gt; could probably be best described as &lt;em&gt;credential request&lt;/em&gt; and &lt;em&gt;credential response&lt;/em&gt;, or, maybe, &lt;em&gt;token request&lt;/em&gt; or &lt;em&gt;token response&lt;/em&gt;. Don’t think of this as &lt;em&gt;just&lt;/em&gt; logging in the user (you do that on the SSO extension). Here, you simply obtain credentials to log that user in later on the SSO Extension.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Besides these endpoints, you need to come up with a way to recognize these tokens when the user authenticates via the SSO extension. More on that later.&lt;/p&gt;

&lt;p&gt;When implementing these endpoints on Keycloak, we also need a client. You need it to attach the tokens you create to the it, as well as to authenticate the user when registering the device/user.  Because it will be used to authentication on a native device (and not on a backend server), It shouldn’t be confidential. Therefore, it uses PKCE with SHA256 for security reasons. As per Apple request, it needs to have the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;urn:apple:platformsso&lt;/code&gt;  scope.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I also recommend adding the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;offline_access&lt;/code&gt;scope. If you don’t, the user will be prompted for a new authentication if the tokens are expired. With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;offline_access&lt;/code&gt;, this won’t happen very often.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This client is used by the SSO extension in two ways:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;to authenticate the user for device and/or user registration (but we don’t &lt;em&gt;have&lt;/em&gt; to - this depends on how you want to associate the user and the IdP, and what checks you make to allow device registration as well. Our Keycloak extension expects a token from this client;&lt;/li&gt;
  &lt;li&gt;as a part of macOS own token retrieval process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The client also needs a single redirect uri, which is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;weblogin-sso://idp-login-redirect&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On our Weblogin SSO Extension, as well as on Keycloak, we used a hardcoded name for the client, so when you create yours, name it &lt;em&gt;psso&lt;/em&gt;. In the future, we will make this configurable.&lt;/p&gt;

&lt;h3 id=&quot;requirements-for-your-psso-extension&quot;&gt;Requirements for your PSSO Extension&lt;/h3&gt;

&lt;p&gt;Well, the PSSO Extension is basically an implementation of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ASAuthorizationProviderExtensionRegistrationHandler&lt;/code&gt; and its methods. The main ones are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beginDeviceRegistration&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beginUserRegistration&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Their implementation is quite similar. What you want to do here is to:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;fetch some keys from the Secure Enclave (their public keys, mind you)&lt;/li&gt;
  &lt;li&gt;Implement some logic for the user authentication&lt;/li&gt;
  &lt;li&gt;send them to the IdP for registration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extension needs to be configured with a profile managed by your MDM. This profile is - but doesn’t have to be - made up from multiple payloads:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;One for your SSO Extension, including the Platform Extension configuration&lt;/li&gt;
  &lt;li&gt;another for the preferences of your application. Here you can save thing you will need on the app, like the URL of your IdP, the client ID, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;You can actually send an array of key/value strings under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ExtensionData&lt;/code&gt;on your SSO Extension configuration, which might make your payload much more manageable. We will do this in the future and read configuration from there instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This configuration will look like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;lt;plist version=«1.0»&amp;gt;
  &amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;PayloadContent&amp;lt;/key&amp;gt;
    &amp;lt;array&amp;gt;
      &amp;lt;dict&amp;gt;
        &amp;lt;key&amp;gt;BaseURL&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;https://&amp;lt;YOURINSTANCE&amp;gt;/realms/&amp;lt;YOURREALM&amp;gt;/&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;Issuer&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;https://&amp;lt;YOURINSTANCE&amp;gt;/&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;Audience&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;psso&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;ClientID&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;psso&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadDisplayName&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;Weblogin SSOE&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadIdentifier&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;mdscentral.00A38C42-503B-4016-A86D-2186CDA5989C.no.uio.WebloginSSO.3E7FAF27-6179-46AA-B1A3-B55E08D3273D&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadOrganization&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadType&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;no.uio.WebloginSSO.ssoe&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadUUID&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;3F7FDF27-6179-46AA-B1A3-B55E08D3273D&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadVersion&amp;lt;/key&amp;gt;
        &amp;lt;integer&amp;gt;1&amp;lt;/integer&amp;gt;
      &amp;lt;/dict&amp;gt;
      &amp;lt;dict&amp;gt;
        &amp;lt;key&amp;gt;PayloadDisplayName&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;Weblogin Platform SSO&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadIdentifier&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;mdscentral.00A38C42-503B-4016-A86D-2186CDA5989C&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadOrganization&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadScope&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;System&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadType&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;Configuration&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadUUID&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;851A1B46-6A8A-442B-91CB-BC12FF416766&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadVersion&amp;lt;/key&amp;gt;
        &amp;lt;integer&amp;gt;1&amp;lt;/integer&amp;gt;
      &amp;lt;/dict&amp;gt;
      &amp;lt;dict&amp;gt;
        &amp;lt;key&amp;gt;AuthenticationMethod&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;UserSecureEnclaveKey&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;ExtensionIdentifier&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;no.uio.WebloginSSO.ssoe&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadDisplayName&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;Weblogin SSO&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadIdentifier&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;com.apple.extensiblesso.CA351D35-96B1-41CF-B25B-DF3273189AAD&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadOrganization&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadType&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;com.apple.extensiblesso&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadUUID&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;4B7148CD-1069-4140-95CE-78F61BCD9C2B&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;PayloadVersion&amp;lt;/key&amp;gt;
        &amp;lt;integer&amp;gt;1&amp;lt;/integer&amp;gt;
        &amp;lt;key&amp;gt;URLs&amp;lt;/key&amp;gt;
        &amp;lt;array&amp;gt;
          &amp;lt;string&amp;gt;https://&amp;lt;YOURINSTANCE&amp;gt;/realms/&amp;lt;YOURREALM&amp;gt;/protocol/&amp;lt;/string&amp;gt;
          &amp;lt;string&amp;gt;https://YOURINSTANCE/realms/&amp;lt;YOURREALM&amp;gt;/psso&amp;lt;/string&amp;gt;
        &amp;lt;/array&amp;gt;
        &amp;lt;key&amp;gt;PlatformSSO&amp;lt;/key&amp;gt;
        &amp;lt;dict&amp;gt;
          &amp;lt;key&amp;gt;AccountDisplayName&amp;lt;/key&amp;gt;
          &amp;lt;string&amp;gt;Universitet i Oslo - Weblogin&amp;lt;/string&amp;gt;
          &amp;lt;key&amp;gt;AuthenticationMethod&amp;lt;/key&amp;gt;
          &amp;lt;string&amp;gt;UserSecureEnclaveKey&amp;lt;/string&amp;gt;
          &amp;lt;key&amp;gt;EnableAuthorization&amp;lt;/key&amp;gt;
          &amp;lt;true /&amp;gt;
          &amp;lt;key&amp;gt;EnableCreateUserAtLogin&amp;lt;/key&amp;gt;
          &amp;lt;true /&amp;gt;
          &amp;lt;key&amp;gt;NewUserAuthorizationMode&amp;lt;/key&amp;gt;
          &amp;lt;string&amp;gt;Groups&amp;lt;/string&amp;gt;
          &amp;lt;key&amp;gt;UseSharedDeviceKeys&amp;lt;/key&amp;gt;
          &amp;lt;true /&amp;gt;
          &amp;lt;key&amp;gt;UserAuthorizationMode&amp;lt;/key&amp;gt;
          &amp;lt;string&amp;gt;Groups&amp;lt;/string&amp;gt;
          &amp;lt;key&amp;gt;AllowDeviceIdentifiersInAttestation&amp;lt;/key&amp;gt;
          &amp;lt;true /&amp;gt;
        &amp;lt;/dict&amp;gt;
        &amp;lt;key&amp;gt;TeamIdentifier&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;YOURTEAM&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;Type&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;Redirect&amp;lt;/string&amp;gt;
      &amp;lt;/dict&amp;gt;
    &amp;lt;/array&amp;gt;
    &amp;lt;key&amp;gt;PayloadDescription&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;PayloadDisplayName&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;Weblogin Platform SSO test/V_41&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;PayloadIdentifier&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;37f5c3b4-36c6-101f-9485-90082e154a1a&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;PayloadOrganization&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;PayloadRemovalDisallowed&amp;lt;/key&amp;gt;
    &amp;lt;false /&amp;gt;
    &amp;lt;key&amp;gt;PayloadType&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;Configuration&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;PayloadUUID&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;dbacb344-7490-4948-b51a-b395d948fd54_41&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;PayloadVersion&amp;lt;/key&amp;gt;
    &amp;lt;integer&amp;gt;1&amp;lt;/integer&amp;gt;
    &amp;lt;key&amp;gt;PayloadScope&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;System&amp;lt;/string&amp;gt;
  &amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;
 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;how--we-did-it-on-the-idp&quot;&gt;How  we did it on the IdP&lt;/h3&gt;

&lt;p&gt;So, I said already that Keycloak is easy to expand, right? So, what we did at first was to create the necessary endpoints. You can see this implementation on &lt;a href=&quot;https://github.com/unioslo/keycloak-psso-extension&quot;&gt;our repo on github&lt;/a&gt;.  Note that this Keycloak extension still needs a few things to be production grade, and we’ll try to point out here what is missing.
All our endpoints are configured as a Keycloak resource. You can see all of them &lt;a href=&quot;https://github.com/unioslo/keycloak-psso-extension/blob/main/src/main/java/no/uio/keycloak/psso/PSSOResource.java&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You are free to give the endpoints any name you wish. On your PSSO Extension, you need to configure the token and the nonce endpoint, as well as the keys endpoint.&lt;/p&gt;

&lt;p&gt;Notice that, for those endpoints where Apple requires a certain standard, you need to accept requests with a few characteristics. Fortunately, Apple tells you how requests should be formed. You can check the documentation, but it is easy to see the format using this command on your terminal: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app-sso  platform -m&lt;/code&gt;, when developing your PSSO Extension.&lt;/p&gt;

&lt;h4 id=&quot;the-nonce-endpoint&quot;&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nonce&lt;/code&gt; endpoint:&lt;/h4&gt;

&lt;p&gt;Apple requires the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nonce&lt;/code&gt; endpoint so that replay attacks can be avoided. So you need to implement some mechanism to receive these requests and return a value.&lt;/p&gt;

&lt;p&gt;How does the Mac send its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nonce&lt;/code&gt; request? &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/obtaining-a-server-nonce&quot;&gt;According to the documentation:&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;POST /oauth2/token HTTP/1.1
Host: auth.example.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
client-request-id: DCAB01D3-B1FE-4E1C-802F-B3EBDCDF9E67
grant_type=srv_challenge 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Send back a json containing a key with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nonce&lt;/code&gt; value. You can configure the name of that key on your PSSO Extension, otherwise &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nonce&lt;/code&gt;is used.&lt;/p&gt;

&lt;p&gt;On our implementation, the endpoint is called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/nonce&lt;/code&gt;.&lt;/p&gt;

&lt;h4 id=&quot;the-device-registration-endpoint&quot;&gt;The device registration endpoint&lt;/h4&gt;

&lt;p&gt;Here, you are free to do as you want. What do you want to do here?&lt;/p&gt;

&lt;p&gt;Basically, you want to receive the request with the device keys and persist them somewhere. You might also want to perform some sort of authentication., otherwise anyone could simple send certificates to your server.&lt;/p&gt;

&lt;p&gt;Apple doesn’t really care how and if you do anything here. Their documentation gives some hints of what you should be doing, but they don’t dive deep into this.
They say you might want to use a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RegistrationToken&lt;/code&gt;, which is something the MDM generates dynamically so that the IdP can use to actually check with the MDM if that device is legit, or you can use &lt;em&gt;device attestation&lt;/em&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.
Since our MDM (Workspace ONE) doesn’t really implement the &lt;/code&gt;RegistrationToken`on its SSO Extension profile, we need to do it the hard way and implement device attestation. more on that later.&lt;/p&gt;

&lt;p&gt;Our endpoint for device registration is called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/enroll&lt;/code&gt;, and it accepts &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST&lt;/code&gt;requests with a json with the following keys/values:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeviceSigningKey&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeviceEncryptionKey&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SignKeyID&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EncKeyID&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attestation&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nonce&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accessToken&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeviceSigningKey&lt;/code&gt;and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeviceEncryptionKey&lt;/code&gt; are used to, well, sign and encrypt login requests and responses between the macOS device and the IdP. Their ID counterparts are used so that you can search for the keys on your database.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attestation&lt;/code&gt;is a cryptographic token that is signed with the private key of your SigningKey (you can use another key here as well) that lives on your Secure Enclave. You can then check this against Apple’s root CA, which we include with our extension. You need to&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AllowDeviceIdentifiersInAttestation&lt;/code&gt; on your configuration profile so that you can extract the serial number and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deviceUDID&lt;/code&gt;from the attestation. This is information that you can use as part of your device management workflows. Our extension requires this.&lt;/p&gt;

&lt;p&gt;On our &lt;a href=&quot;https://github.com/unioslo/weblogin-mac-sso-extension&quot;&gt;Weblogin SSO Extension&lt;/a&gt;, you can see how we generated the keys and the attestation on our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;registerDevice()&lt;/code&gt;method on this &lt;a href=&quot;https://github.com/unioslo/weblogin-mac-sso-extension/blob/main/ssoe/AuthenticationViewController.swift&quot;&gt;file&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You need to send a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nonce&lt;/code&gt; that you previously acquired via the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/nonce&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;You also need to send authenticate the user and send their &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accessToken&lt;/code&gt;. You can also see on our extension how we ask the user to authenticate. You don’t really need to authenticate the user here, actually.  But since the extension doesn’t check, after the attestation,  if that device is “ours” with the MDM, we introduce this authentication - which is something you need to perform for the user anyway when doing the user registration. Attestation tells us the device is legit, and that it is managed.  But it doesn’t prove it is managed by us.&lt;/p&gt;

&lt;p&gt;If you modify the extension, you might remove the accessToken verification and introduce some MDM check.&lt;/p&gt;

&lt;p&gt;This was the only part of this Keycloak extension where we needed to use database storage. Fortunately, this wasn’t super hard to do with Keycloak.&lt;/p&gt;

&lt;h4 id=&quot;the-user-registration-endpoint&quot;&gt;The user registration endpoint&lt;/h4&gt;

&lt;p&gt;This endpoint is called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/enrolluser&lt;/code&gt; and is very similar to the device registration. The keys we send are similar:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nonce&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;userKey&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;userKeyId&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attestation&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accessToken&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What is super cool here is that Keycloak makes it very easy to save the user’s key as a &lt;em&gt;credential&lt;/em&gt;. This allows both admins and users to revoke it on their admin and user GUI, respectively:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../assets/2025/keycloak_psso_account.jpg&quot; alt=&quot;User account console showing the Platform SSO credential&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Keycloak stores this as a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CredentialModel&lt;/code&gt;. 
Here we do need the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accessToken&lt;/code&gt; so that we can confirm the user registering the device really has an account. 
Again, on our companion SSO Extension you can see how we provide the keys and attestation.&lt;/p&gt;

&lt;h4 id=&quot;the-token-endpoint&quot;&gt;The token endpoint&lt;/h4&gt;

&lt;p&gt;Next, we created the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/token&lt;/code&gt; endpoint in order to receive the &lt;em&gt;login request&lt;/em&gt; and send back the &lt;em&gt;login response&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;We created some classes to &lt;a href=&quot;https://github.com/unioslo/keycloak-psso-extension/blob/main/src/main/java/no/uio/keycloak/psso/token/JWSDecoder.java&quot;&gt;validate the request&lt;/a&gt; as &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/creating-and-validating-a-login-request&quot;&gt;suggested by Apple&lt;/a&gt;, and also to &lt;a href=&quot;https://github.com/unioslo/keycloak-psso-extension/blob/main/src/main/java/no/uio/keycloak/psso/token/JweBuilder.java&quot;&gt;build the response&lt;/a&gt; in a &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/creating-a-json-web-encryption-jwe-login-response&quot;&gt;format the macOS will accept&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, the endpoints above is basically what you need to process Platform SSO requests from a Mac.  While the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/enroll&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/enrolluser&lt;/code&gt; are called by your extension when you decide that they should be called, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/nonce&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/token&lt;/code&gt; endpoints need to be written according to Apple specifications.&lt;/p&gt;

&lt;h4 id=&quot;when-does-the-psso-extension-call-these-endpoints&quot;&gt;When does the PSSO extension call these endpoints?&lt;/h4&gt;

&lt;p&gt;So, as I said, you decide when to call the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/enroll&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/enrolluser&lt;/code&gt; endpoints. You might want to call them from your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beginDeviceRegistration&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beginUserRegistration&lt;/code&gt; respectively.  Everytime you start or repair your device registration, the first method is called, and when you register the user - either right after a device registration or later - the second method is called.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/token&lt;/code&gt; method is called:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;when the user authenticates on his mac, by restarting the machine or unlocking it&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/platform-single-sign-on-sso&quot;&gt;by itself when the tokens have expired&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;em&gt;login response&lt;/em&gt; will include the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id_token&lt;/code&gt;and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refresh_token&lt;/code&gt;, and here goes a very special rant from me:&lt;/p&gt;

&lt;p&gt;When using an authentication method that is not the Secure Enclave key, the Platform SSO will call the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/token&lt;/code&gt; endpoint and send the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refresh_token&lt;/code&gt;
in order to get new, fresh &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id_tokens&lt;/code&gt;. But for some reason I don’t understand, it won’t do that with the Secure Enclave. Here is &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/creating-a-refresh-request&quot;&gt;Apple’s  explanation&lt;/a&gt; for that:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;A refresh request uses the previous refresh token to request a new token without prompting the user for credentials. The system attempts it when the existing token hasn’t expired and the time since the last full login hasn’t exceeded the LoginFrequency in the Device Management profile. It doesn’t apply to User Secure Enclave key authentication, &lt;em&gt;because the user isn’t prompted for credentials&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I really don’t get it. What is Apple trying to say here?&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Should I prompt the user for credentials with the Secure Enclave when the refresh token expires?&lt;/li&gt;
  &lt;li&gt;Should we implement some logic on the PSSO extension to update that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id_token&lt;/code&gt;?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I really don’t understand why Apple renews the id token for other authentication methods except for the Secure Enclave.&lt;/p&gt;

&lt;p&gt;This means that we need to either:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;renew the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id_token&lt;/code&gt; ourselves, or&lt;/li&gt;
  &lt;li&gt;disregard the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id_token&lt;/code&gt; and simply use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refresh_token&lt;/code&gt; as an opaque credential.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Grudgingly, we went for the second.  We don’t use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refresh_token&lt;/code&gt; to refresh anything.  We just piggyback on its verifiability on Keycloak, as well as on its long lifetime. If Keycloak is configured to only allow the refresh token to be used once, this extension doesn’t work so well. Luckily, the default configuration of Keycloak is that the reuse of refresh tokens is allowed.&lt;/p&gt;

&lt;p&gt;We might need to revisit this in the future, but we really hope that Apple extends the automatic renewal of refresh tokens to Secure Enclave authentication. It makes more sense to use id tokens for authentication.&lt;/p&gt;

&lt;h4 id=&quot;the-authentication-itself&quot;&gt;The authentication itself&lt;/h4&gt;

&lt;p&gt;One thing that you need to keep in mind:&lt;/p&gt;

&lt;p&gt;In common with the Platform SSO and the SSO Extensions is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loginManager&lt;/code&gt;, an instance of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ASAuthorizationProviderExtensionLoginManager&lt;/code&gt; protocol.  The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loginManager&lt;/code&gt; has access to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/asauthorizationproviderextensionloginmanager/loginconfiguration&quot;&gt;loginConfiguration&lt;/a&gt;, which is where the data regarding your idp, its endpoints, etc, is saved,&lt;/li&gt;
  &lt;li&gt;the &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/asauthorizationproviderextensionuserloginconfiguration&quot;&gt;userLoginConfiguration&lt;/a&gt;, where you can save the username and other claims you need on each login request.&lt;/li&gt;
  &lt;li&gt;the &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/asauthorizationproviderextensionloginmanager/ssotokens&quot;&gt;ssoTokens&lt;/a&gt; - this is where you fetch the tokens you need on your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beginAuthorization&lt;/code&gt; method of your SSO extension.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, when the PSSO fetches the &lt;em&gt;login response&lt;/em&gt;, it saves the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id_token&lt;/code&gt;and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refresh_token&lt;/code&gt; on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssoTokens&lt;/code&gt; member of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loginManager&lt;/code&gt;. That’s where you fetch them if you need them to authenticate the user at the IdP.&lt;/p&gt;

&lt;p&gt;We developed an authenticator, which is how Keycloak calls the diverse methods to verify a user.  Keycloak comes with built-in authenticators, such as username/password, OTP, kerberos, passkeys, etc., and allows developers to code their own.&lt;/p&gt;

&lt;p&gt;Our &lt;a href=&quot;https://github.com/unioslo/keycloak-psso-extension/blob/main/src/main/java/no/uio/keycloak/psso/PSSOAuthenticator.java&quot;&gt;authenticator&lt;/a&gt; inspects the header of the authentication requests and checks if a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Platform-SSO-Authorization&lt;/code&gt; header is present. If so, we evaluate it and authenticate the user.&lt;/p&gt;

&lt;p&gt;The token we send with the header is an encoded json in base64 format, with the following key/values:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refresh_token&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kid&lt;/code&gt; (the Signing Key ID of the device, so that we find which key to use for verifying that this is a legit request)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;signed_at&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;username&lt;/code&gt; (doesn’t really work, it seems Apple doesn’t allow the explicit use of the saved &lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/asauthorizationproviderextensionuserloginconfiguration/loginusername&quot;&gt;loginUserName&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since we added the username to the refresh token, with this data the authenticator will be able to:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;check if the request came from a known device&lt;/li&gt;
  &lt;li&gt;validate the refresh token using Keycloak’s own API&lt;/li&gt;
  &lt;li&gt;consider the user authenticated&lt;/li&gt;
  &lt;li&gt;attach all authentication results from the same device to the same session, which makes it easier to manage sessions (and the reason why we’d love Apple to allow automatic renewal of id tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So our SSO Extension basically does this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;when triggered by a call to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/protocol/openid-connect/auth&lt;/code&gt;, the SSO extension injects the header with our token, as described above,&lt;/li&gt;
  &lt;li&gt;if the response is not the callback with the value from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redirect_uri&lt;/code&gt; with a code, but rather a form with a password field, we display the login window.&lt;/li&gt;
  &lt;li&gt;we return the the browser as soon as we get a redirect to the callback, which is always the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redirect_uri&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It should work a bit the same way with SAML, except it doesn’t.  So, right now, our SAML flow is a bit erratic. If you can help us to fix it, we’d love to hear from you.&lt;/p&gt;

&lt;p&gt;We don’t care about the cookies anymore, because if another application needs the cookies, they simply authenticate again and the extension will give them back with the response.  The SSO is performed by injecting the token into the request, not by keeping cookies. All in all, it will be the same session anyway.&lt;/p&gt;

&lt;h3 id=&quot;a-few-things-that-dont-work-well--yet&quot;&gt;A few things that don’t work well  yet&lt;/h3&gt;

&lt;p&gt;There are a few things that need to be fixed:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;better handling of required actions: Keycloak has some required actions that, when called from their internal clients like the &lt;em&gt;Account console&lt;/em&gt;, seems to create a reauthentication flow that don’t play ball well with our interception of the authentication url. It works perfectly now, but the required action is performed inside the SSO Extension, not on the browser. We’d like to get this done on the browser, but there’s a conflict with cookies that we can’t seem to solve.&lt;/li&gt;
  &lt;li&gt;SAML, as pointed above,&lt;/li&gt;
  &lt;li&gt;the implementation of some security checks during the authentication, the same way that Keycloak does when using their &lt;a href=&quot;https://github.com/keycloak/keycloak/blob/081d8e5a01aa84e04236a8de7adb573dd5c6cc0b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java#L40&quot;&gt;CookieAuthenticator&lt;/a&gt;. This will be done soon, but until then, if your Keycloak instance makes use of ACR/LoA, this authenticator might not comply with your authentication rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;We believe that this implementation might be helpful for a lot of people that want either to try Platform SSO or to provide Platform SSO without having to rely on one of the big IdPs. Theoretically, with a few modifications and by using &lt;a href=&quot;https://www.keycloak.org/securing-apps/token-exchange&quot;&gt;Token Exchange,&lt;/a&gt; , this solution can potentially be used in a way where Keycloak becomes a broker between Macs and other IdPs, but this is not something we tested or implemented.&lt;/p&gt;

&lt;p&gt;It would be very nice if other developers could join our efforts, especially when it comes to the SSO Extension and its processing of SAML flows. If you can and want to help, send PR’s our way or drop as a line on the #Keycloak channel at the MacAdmins &lt;a href=&quot;https://macadmins.slack.com/archives/C09UKEDGBEH&quot;&gt;Slack&lt;/a&gt; .&lt;/p&gt;

&lt;p&gt;Finally,  I just wish Apple could be a bit more explicit on how they believe this extension should be used:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;After a token expiration, should we renew automatically for the user (after all, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loginManager&lt;/code&gt; has a method for that,  or should the user authenticate manually?&lt;/li&gt;
  &lt;li&gt;How should we handle SAML flows?&lt;/li&gt;
  &lt;li&gt;Why no refreshing of tokens for Secure Enclave flows?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think there’s a lot that could be accomplished here if some of Apple’s &lt;em&gt;intentions&lt;/em&gt; were known. But i must admit that, after implementing this extension, a lot of the documentation makes more sense - in the beginning, it felt insufficient, but I guess that’s mostly because there are no examples or design patterns shown anywhere, except by those examples from Twocanoes.&lt;/p&gt;

&lt;h3 id=&quot;acknowledgments&quot;&gt;Acknowledgments&lt;/h3&gt;

&lt;p&gt;We are very grateful to the work of Timothy Perfitt and Joel Rennich, who across presentations and articles made this subject a bit clearer to a lot of people, myself included.&lt;/p&gt;

&lt;p&gt;I am also grateful to my colleagues Gaute and Thomas, who encouraged me to write this, and who came with good ideas and feedback along the way.&lt;/p&gt;
</description>
        <pubDate>Sat, 22 Nov 2025 14:30:23 +0100</pubDate>
        <link>https://francisaugusto.com/2025/Platform_single_sign_on_diy/</link>
        <guid isPermaLink="true">https://francisaugusto.com/2025/Platform_single_sign_on_diy/</guid>
        
        <category>keycloak</category>
        
        <category>idp</category>
        
        <category>apple</category>
        
        
        <category>technology</category>
        
      </item>
    
      <item>
        <title>How to use Keycloak as an IdP for Entra ID via ADFS</title>
        <description>&lt;h3 id=&quot;what&quot;&gt;What?&lt;/h3&gt;
&lt;p&gt;This is a post about how to use Keycloak (or another SAML-supporting IdP) as an IdP for Entra ID via ADFS. Or, putting it in more technically correct terms, how to use Keycloak as an IdP for an ADFS instance that is federated with Entra ID.&lt;/p&gt;

&lt;p&gt;I decided to write this after suffering a lot to find good documentation and/or reports on how this should be done.&lt;/p&gt;

&lt;h3 id=&quot;disclaimers&quot;&gt;Disclaimers&lt;/h3&gt;
&lt;p&gt;I think it is fair to say a few things before we get started:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I don’t like Microsoft. You’ll see a bit why when you keep reading this document.&lt;/li&gt;
  &lt;li&gt;I am not, and never have been, a Windows user. So my experience on Windows is quite limited&lt;/li&gt;
  &lt;li&gt;This document represents my own opinions and ideology, and not those of my employer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to my awesome Windows colleagues who helped me testing a few of these things that I describe here.&lt;/p&gt;

&lt;h3 id=&quot;why&quot;&gt;Why?&lt;/h3&gt;
&lt;p&gt;I am glad you ask.&lt;/p&gt;

&lt;p&gt;You see, there are many reasons why you’d want to federate your authentication when using Microsoft:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;You might have your on-premise Keycloak or another IdP, but you also have Microsoft services, and users end up having two different authentication portals;&lt;/li&gt;
  &lt;li&gt;You don’t like ADFS’ own authentication portal;&lt;/li&gt;
  &lt;li&gt;You certainly don’t want to export your password hashes to Microsoft, especially in these Trump-times.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The good thing is that by going this route, your users will have single-sign on between your Microsoft and their apps (including those you register on Entra ID) and your Keycloak-based authentication provider and apps that use its instance for authentication.&lt;/p&gt;

&lt;p&gt;The bad thing is that you need to choose what to do with 2FA. Will you keep it on Microsoft? Will you use Keycloak’s 2FA options? Either way, there are options we will discuss.&lt;/p&gt;

&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Suppose your organization uses Entra ID, and it isn’t ready to sync your users’ passwords to your Entra/Azure tenant. You still want to provide on-prem authentication.&lt;/p&gt;

&lt;p&gt;Entra ID allows this in what is called “federated” authentication. But this is Microsoft: they simply are unable to use their definitions in a consistent way. If you google “federated authentication microsoft”, you will see that what they call “federated authentication” can be two things:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Having authentication delegated to an external IdP, as described &lt;a href=&quot;https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/whatis-fed&quot;&gt;here&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Having an external IdP configured for &lt;em&gt;external&lt;/em&gt; users, ie, not your own users, as described &lt;a href=&quot;https://learn.microsoft.com/en-us/entra/external-id/direct-federation&quot;&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are interested in authenticate our own users on our premises. This is something Microsoft does not document well and they don’t really go out of their way to help with this, since they really want you to export your password hashes to the cloud. But it is &lt;a href=&quot;https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-fed-saml-idp&quot;&gt;supported&lt;/a&gt; nevertheless.&lt;/p&gt;

&lt;p&gt;So you have two (actually three) options here if you want to use on-premise authentication:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;use the good’ol ADFS;&lt;/li&gt;
  &lt;li&gt;use Keycloak directly, by configuring Keycloak as a SAML federated IdP for Entra ID;&lt;/li&gt;
  &lt;li&gt;use the good’ol ADFS, but configuring it to use Keycloak as its IdP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why don’t you just federate with Keycloak directly? Well, because Microsoft. They list some limitations on the &lt;a href=&quot;https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-fed-saml-idp&quot;&gt;document about how to configure a SAML IdP&lt;/a&gt;, but the real deal is if you use Intune with Entra ID, device registration won’t work if the IdP you use does not support the protocol who is dying but Microsoft keeps it breathing: WS-Fed. You can read about this limitation &lt;a href=&quot;https://learn.microsoft.com/en-us/education/windows/federated-sign-in?tabs=intune&quot;&gt;here&lt;/a&gt;, but the problem is that nobody, including Keycloak, supports WS-Fed.&lt;/p&gt;

&lt;p&gt;But not everything is lost: you can still use ADFS (which also refuses to die), and configure it to redirect your users to your on-premises IdP - in our case, Keycloak. So while you won’t get rid of ADFS, it will give you the compatibility you need.&lt;/p&gt;

&lt;h3 id=&quot;configuring-adfs-to-use-keycloak-as-an-idp&quot;&gt;Configuring ADFS to use Keycloak as an IdP&lt;/h3&gt;

&lt;p&gt;Here we need to clarify a few definitions, because Microsoft refuses to call things like everyone else.&lt;/p&gt;

&lt;p&gt;Look at this table:&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Microsoft&lt;/td&gt;
      &lt;td&gt;SAML&lt;/td&gt;
      &lt;td&gt;OAuth2&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Claims Provider Trust&lt;/td&gt;
      &lt;td&gt;SSO IdP&lt;/td&gt;
      &lt;td&gt;IdP/Resource server/&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Relying Party Trust&lt;/td&gt;
      &lt;td&gt;SSO SP (Service Provider)&lt;/td&gt;
      &lt;td&gt;client&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;So here what you need is to configure Keycloak as a Claims Provider Trust (CPT) on your ADFS. A Claims Provider Trust is basically an IdP. The default CPT in ADFS is Active Directory, but you can configure a SAML (or a WS-Fed) IdP there as well.&lt;/p&gt;

&lt;p&gt;A Relying Party Trust (RPT) is an application that authenticates via your ADFS. In other words, they authenticate on one (or more) of your CPT’s. In SAML lingo, they are SP (Service Providers). In the scenario we are talking about here, where Entra ID will send users to its federated authentication IdP, the RPT is… Entra ID! But you probably know that by now: when you use Entra Connect to configure User sign in so that users will authenticate on ADFS, it creates an RPT that points to Entra ID on your ADFS.&lt;/p&gt;

&lt;p&gt;So, without further ado, how do you add Keycloak as an IDP?&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;open ADFS, click on Claims Provider Trust on your left, and then Add Claims Provider Trust on your right&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;../../assets/2025/adfs/configure-cpt.png&quot; alt=&quot;Create a new CPT&quot; title=&quot;Choose to add a new CPT&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;On the window that shows up, choose “Start”&lt;/li&gt;
  &lt;li&gt;Enter the SAML metadata for your keycloak instance and realm, something like https://&lt;mykeycloak&gt;/realms/&lt;myrealm&gt;/protocol/saml/descriptor:&lt;/myrealm&gt;&lt;/mykeycloak&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;../../assets/2025/adfs/add-keycloak.png&quot; alt=&quot;Enter Keycloak’s metadata url&quot; title=&quot;Add Keycloak as a CPT&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;follow the additional steps which are self explanatory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There, now Keycloak is an IdP. But you are far from done, my friend. This is Microsoft, and you need to get into the Microsoft way of doing things…&lt;/p&gt;

&lt;h3 id=&quot;claims-claims-and-more-claims&quot;&gt;Claims, claims and more claims&lt;/h3&gt;

&lt;p&gt;Coming from Keycloak world, you are used to something called account matching: you can configure Keycloak in a way that, when you use an external IdP, if the username or e-mail of the external user already exists on your user base, it will then establish a link between the external and internal users, so that the internal user’s attributes are the ones that Keycloak sends to the applications. This is a way to simplify it, things can be configured differently, but the point is that a pretty default way of using keycloak is establishing links between internal and external users.&lt;/p&gt;

&lt;p&gt;ADFS works very differently. When you configure a CPT, the user claims received from Keycloak are not used to fetch a user and all their attributes. If you want to match a Keycloak user to an Active Directory user (and you know you want that), you need to match &lt;em&gt;claim by claim&lt;/em&gt;. You cannot simply say “Oh, fetch me the user myuser@mydomain.com and their attributes”. You need, in a way, reconstruct the claims your RPT needs.&lt;/p&gt;

&lt;p&gt;When configuring Keycloak to be used as an IdP for ADFS on an Entra ID scenario, you will see that Entra ID expects a lot of claims in a very special format, something like that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress&lt;/code&gt;. So you need to provide a claim like that.&lt;/p&gt;

&lt;p&gt;Let’s see how you do that:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Add Claim rules to your newly created CPT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;../../assets/2025/adfs/add-claim-rules.png&quot; alt=&quot;Add claim rules&quot; title=&quot;Add Claim rules&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Click on “Add rule” on the window that shows up.&lt;/li&gt;
  &lt;li&gt;Choose “Send claims Using a Custom Rule”:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;../../assets/2025/adfs/send-claims.png&quot; alt=&quot;Send claims as a custom rule&quot; title=&quot;Custom rules&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I am using custom rules here because I need to translate the claims from Keycloak to those used by Entra ID. I could use other types of rules (like for example “passthrough” if the claims already come with the right claim name from Keycloak. This is really up to you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You’ll get a window like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../assets/2025/adfs/custom-rule.png&quot; alt=&quot;Custom rule&quot; title=&quot;Custom rule&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Here you need to enter something in a Microsoft ADFS Claim language. Basically what you want is to say:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Which claim received from Keycloak you want to do something about;&lt;/li&gt;
  &lt;li&gt;How that claim will be called (and made available to) the RPT’s.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s start with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;userPrincipalName&lt;/code&gt;claim. Entra ID requires the so-called UPN in a few formats. So if you have a claim in keycloak that gets your user in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user@domain&lt;/code&gt;format, you fetch it, and give it a name Entra ID will like. For example:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;c:[Type == &quot;userPrincipalName&quot;]
 =&amp;gt; issue(store = &quot;Active Directory&quot;, types = (&quot;http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress&quot;, &quot;http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname&quot;, &quot;http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname&quot;, &quot;http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn&quot;,
&quot;http://schemas.xmlsoap.org/claims/UPN&quot;), 
query = &quot;(userPrincipalName={0});mail,givenName,sn,userPrincipalName,userPrincipalName;MYDOMAIN\adfs&quot;, param = c.Value);

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Ok, this is confusing. What happens here?&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I get a claim from Keycloak called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;userPrincipalName&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;I query that claim in Active Directory to find a user that matches it&lt;/li&gt;
  &lt;li&gt;I assign this user’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mail&lt;/code&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;givenName&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sn&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;userPrincipalName&lt;/code&gt; to the clames on the “types” array.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, you can simply send the claims with values you fetch from Keycloak, this is fine. But I’d rather use Active Directory as an authoritative source for the user’s identity.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MYDOMAIN\adfs&lt;/code&gt; is something not clear to me. It seems that you put any user here, and it will work - as a sort of placeholder. But in some documents I read this needs to be an existing user, specifically one that binds to AD. I admit I am not sure, so I just used one I created when configuring ADFS. Again, Microsoft’s documentation is not very nice here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You still need to add two more rules. After saving the first one, add another rule, like the first one, but with the following content:  &lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;c:[Type == &quot;userPrincipalName&quot;]
 =&amp;gt; issue(store = &quot;Active Directory&quot;, types = (&quot;http://schemas.microsoft.com/LiveID/Federation/2008/05/ImmutableID&quot;), query = &quot;(userPrincipalName={0});ms-DS-ConsistencyGuid;MYDOMAIN\adfs&quot;, param = c.Value);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Again you use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;userPrincipalName&lt;/code&gt;to fetch an attribute that will be used as the ImmutableID.&lt;/p&gt;

&lt;p&gt;The last mandatory rule is this one:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;c:[Type == &quot;userPrincipalName&quot;]
 =&amp;gt; issue(Type = &quot;http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname&quot;, Value = &quot;MYDOMAIN\&quot; + regexreplace(c.Value, &quot;^(.*?)@.*$&quot;, &quot;$1&quot;));
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You are good to go! Now you’re ready to test it. Go to https://login.microsoftonline.com, enter your username, and you will be redirect to your Keycloak instance. Or maybe you will see a list with your Keycloak instance name and Active Directory. This is fine for testing, but I guess you want the user to be sent right away to Keycloak.&lt;/p&gt;

&lt;p&gt;If so, use the following command:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Set-AdfsProperties -EnableLocalAuthenticationTypes $false
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;I configured way more claims on my test instance. That’s because I see that Entra ID, on its RPT’s Claims Issuance Policy, uses a few other claims that Active Directory has, but Keycloak doesn’t, such as primary group, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;objectguid&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;msdsconsitencyguid&lt;/code&gt;, etc. But to be honest Ii am not sure you need all those. At the end of this articlel I give you a list of all the rules I configured, and you can decide if you want to configure them or not.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;2fa-or-not-2fa-thats-the-question&quot;&gt;2FA or not 2FA? That’s the question&lt;/h3&gt;

&lt;p&gt;Now, suppose you have 2FA configured in Keycloak, or in Entra, or in both. You need an strategy here: are you going to use Keycloak’s, Microsoft Entra’s, or both? Both is not ideal, of course.&lt;/p&gt;

&lt;p&gt;The issue here is that you might want to give up Microsoft’s 2FA. For security reasons, you might not want to turn off Keycloak’s 2FA for the ADFS client. You want to make sure that a Keycloak user who authenticated via Keycloak has used 2FA.&lt;/p&gt;

&lt;p&gt;The problem is this: suppose you turn off 2FA in Keycloak for the ADFS authentication, relying on Entra ID’s 2FA. The user will authenticate in Keycloak, but just with username and password. The same user don’t go ahead with Entra ID authentication. But he is already fully authenticated in Keycloak! So if he goes ahead to another application, he’ll be already logged in, thus skipping 2FA.&lt;/p&gt;

&lt;p&gt;So that’s why you want to keep 2FA in Keycloak, but, in such integration you might want to send a message to Entra saying “Never mind, this user has already used 2FA with me, let him without further ado”.&lt;/p&gt;

&lt;p&gt;This is something where the documentation fails blatantly. So what you have to do is to add a Claim Issuance Policy to your RPT (in this case, Entra’s RPT, usually called “Microsoft Office 365 Identity Platform Worldwide”, and add a rule there:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../assets/2025/adfs/add-rpt-rule.png&quot; alt=&quot;Add RPT rule&quot; title=&quot;Add RPT rule&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Then choose “Add rule”, “Sending claims using a custom rule”, and enter the following rule:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;=&amp;gt; issue(Type = &quot;http://schemas.microsoft.com/claims/authnmethodsreferences&quot;, Value = &quot;http://schemas.microsoft.com/claims/multipleauthn&quot;);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;Please notice that this will also make Entra ID stop asking for 2FA even if you actually authenticate from Active Directory. To avoid that, don’t configure the rule above. Instesd, configure it on the CPT, and add it again to the RPT, like this:&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;CPT:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;=&amp;gt; issue(Type = &quot;http://schemas.microsoft.com/claims/authnmethodsreferences&quot;, Value = &quot;http://schemas.microsoft.com/claims/multipleauthn&quot;);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;RPT:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;c:[Type == &quot;http://schemas.microsoft.com/claims/authnmethodsreferences&quot;, Issuer = &quot;mykeycloakcpt&quot;]
 =&amp;gt; issue(claim = c);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Remember to replace “mykeycloakcpt” for the name of your CPT that has your Keycloak configuration.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;This was just another example how Microsoft’s documentation can be misleading. The documenttaion found &lt;a href=&quot;https://learn.microsoft.com/en-us/entra/identity/authentication/how-to-mfa-expected-inbound-assertions&quot;&gt;here&lt;/a&gt; never worked. It seems the claims were all wrong. It was only after seing how Ping does it that we got it right:
&lt;a href=&quot;https://support.pingidentity.com/s/article/PingID-as-on-premises-MFA-for-federated-Office-365-users&quot;&gt;See here.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Oh, and run these Powershell commands:&lt;/p&gt;

&lt;p&gt;Run these two commands on Powershell:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Update-MgDomainFederationConfiguration -DomainId &amp;lt;yourdomain&amp;gt; -InternalDomainFederationId  &amp;lt;yourdomainfederationid&amp;gt; -PromptLoginBehavior nativeSupport
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Update-MgDomainFederationConfiguration -DomainId &amp;lt;yourdomain&amp;gt; -InternalDomainFederationId  &amp;lt;yourdomainfederationid&amp;gt; -federatedIdpMfaBehavior acceptIfMfaDoneByFederatedIdp 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;I hope this helps you to configure Keycloak as an IdP for Entra ID. It’s recommended to test this with a lot of authentication scenarioes, like e-mail, device enrollment, etc, to see if everything works.&lt;/p&gt;

&lt;h3 id=&quot;appendix&quot;&gt;Appendix&lt;/h3&gt;

&lt;p&gt;Here is a list of other CPT rules I configured, though I’m almost sure we din’t need them:&lt;/p&gt;

&lt;p&gt;[](msdsconsitencyguid:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;c:[Type == &quot;userPrincipalName&quot;]
 =&amp;gt; issue(store = &quot;Active Directory&quot;,
          types = (&quot;http://schemas.microsoft.com/ws/2016/02/identity/claims/msdsconsistencyguid&quot;),
          query = &quot;(userPrincipalName={0});ms-DS-ConsistencyGuid;MYDOMAIN\adfs&quot;,
          param = c.Value);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;objectguid:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;c:[Type == &quot;http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn&quot;]
 =&amp;gt; issue(store = &quot;Active Directory&quot;,
          types = (&quot;http://schemas.microsoft.com/ws/2008/06/identity/claims/objectguid&quot;),
          query = &quot;(userPrincipalName={0});objectGUID;MYDOMAIN\adfs&quot;,
          param = c.Value);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;groupsid:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;c:[Type == &quot;http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn&quot;]
 =&amp;gt; issue(store = &quot;Active Directory&quot;,
          types = (&quot;http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid&quot;),
          query = &quot;(userPrincipalName={0});tokenGroupsSid;MYDOMAIN\adfs&quot;,
          param = c.Value);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Account type:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;c:[Type == &quot;http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn&quot;]
 =&amp;gt; issue(store = &quot;Active Directory&quot;,
          types = (&quot;http://schemas.microsoft.com/ws/2012/01/accounttype&quot;),
          query = &quot;(userPrincipalName={0});objectClass;MYDOMAIN\adfs&quot;,
          param = c.Value);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;onpremobjectguid:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;c:[Type == &quot;userPrincipalName&quot;]
 =&amp;gt; issue(store = &quot;Active Directory&quot;, types = (&quot;http://schemas.microsoft.com/identity/claims/onpremobjectguid&quot;), query = &quot;(userPrincipalName={0});objectGUID;MYDOMAIN\adfs&quot;, param = c.Value);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;primarysid:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;
c:[Type == &quot;userPrincipalName&quot;]
 =&amp;gt; issue(store = &quot;Active Directory&quot;, types = (&quot;http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid&quot;), query = &quot;(userPrincipalName={0});objectSid;MYDOMAIN\adfs&quot;, param = c.Value);
&lt;/code&gt;)&lt;/p&gt;
</description>
        <pubDate>Wed, 11 Jun 2025 22:30:23 +0200</pubDate>
        <link>https://francisaugusto.com/2025/How-to-use-Keycloak-as-idp-for-ADFS-Entra/</link>
        <guid isPermaLink="true">https://francisaugusto.com/2025/How-to-use-Keycloak-as-idp-for-ADFS-Entra/</guid>
        
        <category>entra</category>
        
        <category>microsoft</category>
        
        <category>keycloak</category>
        
        <category>idp</category>
        
        <category>saml</category>
        
        
        <category>technology</category>
        
      </item>
    
      <item>
        <title>E-mail authentication, quo vadis? (Or where is OIDC/Oauth2 for everyone?)</title>
        <description>&lt;p&gt;There’s something rotten in the world of e-mail.&lt;/p&gt;

&lt;p&gt;You see, if we come back twenty-five years ago, e-mail was handled in-house by companies or by your local ISP. People did have their hotmail or yahoo accounts, but you’d always get one from your ISP.&lt;/p&gt;

&lt;p&gt;Then companies like Google started to host e-mail for companies, but it was still possible to run your own e-mail server. Heck, it is still is.&lt;/p&gt;

&lt;p&gt;But a certain challenge came in: multi-factor authentication. Today, MFA or 2FA are absolute requirements for any authentication, and e-mail is not an exception.&lt;/p&gt;

&lt;p&gt;Suddenly, it got hard to use plain username/password login for IMAP and SMTP, the most widely used e-mail protocols if the goal was to have 2FA. The alternatives would be adding 2FA to a password, but that would prompt the user every now and then to reenter the password with the OTP.&lt;/p&gt;

&lt;p&gt;That’s why over the last few years the big providers moved for OAuth2 authentication: the user credentials in form of username/password aren’t sent to the e-mail server anymore: the user is redirect to a browser, then he authenticates on his IdP (Identity Provider), get a token which is then sent to the e-mail server.&lt;/p&gt;

&lt;p&gt;Luckily, some of the most popular e-mail servers do support OAuth2. But unluckly, nome of them allow you to add the e-mail server you want. What happened here is that e-mails usually come with a pre-configured list of OAuth2 providers (Google, Microsoft, Yahoo, etc). This is strenghtening e-mail as provided by the Big One’s and preventing anyone to have an e-mail server with modern authentication.&lt;/p&gt;

&lt;p&gt;This is partially a problem due how Oauth2 works: it requires an IdP to configure a client, and e-mail clients need information about that client to talk with them - often with a client secret.&lt;/p&gt;

&lt;p&gt;So far, no e-mail client that I know of allow you to simple enter the OAuth2 configuration of your IdP of choice. The Big One’s have their credentials baked in the different e-mail clients. See, for example, this source code from &lt;a href=&quot;https://github.com/mozilla/releases-comm-central/blob/462d143ffcdc2028d918454d01c8b8c638eda9a5/mailnews/base/src/OAuth2Providers.sys.mjs&quot;&gt;Thunderbird&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Just for fun, I tried to bake my own IdP on the source code of Thunderbird. And it worked very well! And it begs the question: why can’t people add their arbitrary IdP on e-mail configurations? If you can choose the authentication method between username-and-password, kerberos, certificates, etc, why can’t you choose OAuth2 and configure the clients accordingly?&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../assets/2025/oauth-on-thunderbird.png&quot; alt=&quot;Thunderbird using another OAuth2 IdP&quot; title=&quot;Thunderbid using another IdP with OAuth2...&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To be clear, this is not so much of a problem for webmail. Many webmail solutions like Roundcube support OAuth2. I configured it to use OAuth2 against my Dovecot server, and boom, I get single sign-on between all my applications. But saddly, no e-mail client seems to allow adding OAuth2 as an authentication type. Either it comes pre-configured by default for the Big Ones, or you can’t add it.&lt;/p&gt;

&lt;p&gt;Unfortunately, it doesn’t seem to be getting any better, by judging from this &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1602166&quot;&gt;five year long thread&lt;/a&gt; about implementing OAuth2 on Thunderbird. If an open-source client, which should attempt to serve the open-source cause, isn’t supporting it, why should those default clients tied to the Big Ones?&lt;/p&gt;

&lt;p&gt;What happens? The Enterprise world is led to believe that they &lt;em&gt;need&lt;/em&gt; to get e-mail from the Big Ones, as nobody seems to be able to give them 2FA for e-mail. E-mail providers’ diversity shrinks.&lt;/p&gt;

&lt;p&gt;I loved the work of Michael W Lucas, especially his &lt;a href=&quot;https://mwl.io/nonfiction/tools#ryoms&quot;&gt;Run Your Own Mail Server&lt;/a&gt; book. We need to get e-mail diversity back. But the world is moving on. New protocols such as &lt;a href=&quot;https://jmap.io/software.html&quot;&gt;Jmap&lt;/a&gt; start to get attention. Still, modern authentication isn’t available for everyone on the e-mail world, unless your e-mail is hosted by the big ones.&lt;/p&gt;

&lt;p&gt;This has to change.&lt;/p&gt;
</description>
        <pubDate>Sun, 02 Feb 2025 10:15:23 +0100</pubDate>
        <link>https://francisaugusto.com/2025/Email-quo-vadis-or-where-is-oidc-for-everyone/</link>
        <guid isPermaLink="true">https://francisaugusto.com/2025/Email-quo-vadis-or-where-is-oidc-for-everyone/</guid>
        
        <category>e-mail</category>
        
        <category>oidc</category>
        
        <category>oauth2</category>
        
        <category>thunderbird</category>
        
        
        <category>technology</category>
        
      </item>
    
      <item>
        <title>The complexities of running a Fediverse instance on clusters</title>
        <description>&lt;h4 id=&quot;disclaimer&quot;&gt;Disclaimer&lt;/h4&gt;

&lt;p&gt;I am no expert on Kubernetes and neither have I a deeper understand of all the components of the different Fediverse servers. What I am writing here is a result of my own impressions, which may be incorrect or misleading.&lt;/p&gt;

&lt;h4 id=&quot;introduction&quot;&gt;Introduction&lt;/h4&gt;

&lt;p&gt;I started to run Fediverse instances in november 2022. Our Mastodon instance, &lt;a href=&quot;https://babb.no&quot;&gt;Babb.no&lt;/a&gt; has been running since. Since then, I also started to run a &lt;a href=&quot;https://pixelfed.babb.no&quot;&gt;Pixelfed instance&lt;/a&gt;, a &lt;a href=&quot;https://books.babb.no&quot;&gt;Bookwyrm instance&lt;/a&gt; and finally a &lt;a href=&quot;https://social.babb.no&quot;&gt;Friendica instance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My goal was to offer colleagues and friends a gateway to healthier social networks. Well, those didn’t come, so I opened these networks to everyone.&lt;/p&gt;

&lt;p&gt;Since I don’t have exaactly so much free time to understand these services and fix or scale them when the need that, it is important for me that I can run those services on a kubernetes cluster, to have a nice forum to exchange information on how to fix things, and to have good documentation.&lt;/p&gt;

&lt;p&gt;Sadly, these services are not always easy to understand. They have sometimes a lot of components and it is not trivial to understand what they do nor how they relate to each other.&lt;/p&gt;

&lt;p&gt;I want to compare how these instances in three areas:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;cluster deployability&lt;/li&gt;
  &lt;li&gt;documentation&lt;/li&gt;
  &lt;li&gt;support community for sysadmins&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;cluster-deployability&quot;&gt;Cluster deployability&lt;/h4&gt;

&lt;p&gt;These days, enterprise environments for web services are hosted on cluster like Kubernetes.&lt;/p&gt;

&lt;p&gt;However, because Fediverse instances are primarily run by individuals with limited access to resources, I suspect that there are few tutorials, if at all, on how to run Fediverse instances on a cluster.&lt;/p&gt;

&lt;p&gt;Among all the servers, things vary considerably. From having a helm chart, to not even mentioning clusters at all.&lt;/p&gt;

&lt;p&gt;The main problem here is that there is very little documentation on which  components can or need to be deployed separately so that one can use cluster for redundancy and high availability.&lt;/p&gt;

&lt;p&gt;Mastodon is clearly the winner here. It provides Helm charts, so one can have things deployed quite easily. However, I didn’t go that route. I used their Docker compose file, read a lot of posts about mastodon sidekiq workers, and created my yaml files using &lt;a href=&quot;https://kompose.io&quot;&gt;Kompose&lt;/a&gt;. It gave lots of flexibility back in the days, though today I’d probably force myself to understand Helm.&lt;/p&gt;

&lt;p&gt;Pixelfed didn’t have an official Docker image back in the days, so I got a Dockerfile somewhere, and deployed it myself. Luckly, its design is simple - one needs a web container and a worker container. Both can scale, and it works fine.&lt;/p&gt;

&lt;p&gt;Bookwyrm wasn’t so easy either, to the point I created my &lt;a href=&quot;https://github.com/oculos/bookwyrm-kubernetes&quot;&gt;own repo&lt;/a&gt; with configuration files.&lt;/p&gt;

&lt;p&gt;The problem with Bookwyrm, and as well as with Friendica, is that while they do provide ready-made docker images, they are based on the premise of a single node deployment. So we end up having a redundant Nginx container, and on neither of those we have a clear idea on a good deployment architecture.&lt;/p&gt;

&lt;p&gt;A little digression here: people deploy clusters differently when it comes to web access: some have an ingress (Nginx) exposed to the world, some have a reverse proxy pointing to services, some (like me) have a reverse proxy that proxy requests to an ingress.&lt;/p&gt;

&lt;p&gt;So a clear documentation on what is the best way to implement routes, block paths, etc, would be great for cluster maintainers.&lt;/p&gt;

&lt;p&gt;Bookwyrm is probably scalable - I never tried to increase the number of its workers, but I see no reason why it wouldn’t work.&lt;/p&gt;

&lt;p&gt;Friendica was the hardest due its confusing docker documentation, no good php-fpm tweaks (like for example fixing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pm.max_children&lt;/code&gt; needs to be done manually) and no scalability whatsoever. I guess that at some point I’ll have to limit the number of new users. One may most likely increase the Nginx pods, but there is no worker scalability.&lt;/p&gt;

&lt;h4 id=&quot;documentation&quot;&gt;Documentation&lt;/h4&gt;

&lt;p&gt;While none of those servers have cluster-specific documentation, they have some documentation of configuration items.&lt;/p&gt;

&lt;p&gt;Mastodon is again the winner. With a mature structure and development, it has an up-to-date documentation on all configuration options, though some are not explained on the list of configuration parameters. For example, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PAPERCLIP_ROOT_URI&lt;/code&gt; is listed as valid configuration, but you’d have no idea of what it is used for unless you read its Object storage configuration (even though this applies to local, filesystem storage).&lt;/p&gt;

&lt;p&gt;Bookwyrm has a good installation, but not good index of configuration options, which is useful when converting installation patterns to a cluster.&lt;/p&gt;

&lt;p&gt;Pixelfed was very good before, but the documentation is so outdated these days that some configuration is only found on the source code. Fortunately, the main developer is accessible and usually helps right away.&lt;/p&gt;

&lt;p&gt;Friendica has documentation on different sites, which is confusing, as you don’t know what’s current and what’s old. It’s a wiki and documentation site. This makes find things difficult&lt;/p&gt;

&lt;h4 id=&quot;community-and-help-for-sysadmins&quot;&gt;Community and help for sysadmins&lt;/h4&gt;

&lt;p&gt;If you’re a Patreon to Mastodon, you get access to their Discord. There, there’s a channel for sysadmins (I miss the term “sysops”), and one gets a lot of help. It has saved me many times, so I feel a bit safer knowing I’m not on my own.&lt;/p&gt;

&lt;p&gt;Pixelfed also has a Discord and a channel there for sysadmins. However, help, true help, comes only from it’s main (only?) writer. So one’s milleage may vary: if you need help when it’s night in Canada, you’ll have to wait. Unfortunately, Pixelfed is highly dependant on one developer, and it’s impressive that, in spite of this dependancy, things go well and one does get help. It just might take a bit of time.&lt;/p&gt;

&lt;p&gt;Bookwyrm has a Matrix room for admins, but seldom one gets help there. Sometimes posting on their github issues will get attention, but it might take weeks. I feel that development there is quite stalled, but I hope I’m wrong.&lt;/p&gt;

&lt;p&gt;Friendica was very difficult for me one year ago when I tried for the first time. Nobody was running it on Kubernetes. Replies were vague, and it was hard to get help. I gave it a try again recently, and, while I haven’t anyone running it on a cluster, I did get better help this time on their group on, well, on Friendica. They do have a Matrix room, but I didn’t get too much help there.&lt;/p&gt;

&lt;h4 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h4&gt;

&lt;p&gt;I want to make clear all this is based on my own experience, and it might be based on misunderstanding on where to get help or configuration information.&lt;/p&gt;

&lt;p&gt;That said, if you’re planning on deploying and maintaining a Fediverse instance on a cluster or any other deployment that’s different than the instance’s default install instructions, you might be on your own.&lt;/p&gt;

&lt;p&gt;Mastodon, being much more mature than the others, have a robust community of developers and seasoned admins who almost certainly will help you.&lt;/p&gt;

&lt;p&gt;Unfortunately, that can’t be said about the other fediverse services. I do hope that these services gain critical mass so that they’ll get more resources and make them easier to administer.&lt;/p&gt;
</description>
        <pubDate>Sun, 19 Jan 2025 17:01:23 +0100</pubDate>
        <link>https://francisaugusto.com/2025/Complexity-of-running-a-fediverse-instance/</link>
        <guid isPermaLink="true">https://francisaugusto.com/2025/Complexity-of-running-a-fediverse-instance/</guid>
        
        <category>kubernetes</category>
        
        <category>fediverse</category>
        
        <category>mastodon</category>
        
        <category>pixelfed</category>
        
        <category>bookwyrm</category>
        
        <category>friendica</category>
        
        
        <category>personal</category>
        
      </item>
    
      <item>
        <title>Installing Matrix on Kubernetes - what they don't tell you</title>
        <description>&lt;h4 id=&quot;disclaimer&quot;&gt;Disclaimer&lt;/h4&gt;

&lt;p&gt;I am no expert on Matrix or on Kubernetes. I just happen to use Kubernetes a bit, and got recently hooked on Matrix.&lt;/p&gt;

&lt;p&gt;This is not a complete manual or guide on how to install Matrix on kubernetes. It is rather an introduction on the challenges you might experience and a heads up so you don’t make the same mistakes I made.&lt;/p&gt;

&lt;h4 id=&quot;why-are-you-writing-this&quot;&gt;Why are you writing this?&lt;/h4&gt;

&lt;p&gt;I wanted to install Matrix. I have install (and still maintain) a few opensource-based projects, specially Fediverse instances. I thought, what can’t wrong, right?&lt;/p&gt;

&lt;p&gt;In my mind, these services follow the same design pattern, which is basically:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;a web-server/streaming service&lt;/li&gt;
  &lt;li&gt;a few workers&lt;/li&gt;
  &lt;li&gt;redis&lt;/li&gt;
  &lt;li&gt;database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While Synapse (one of the implementations of the Matrix protocol) has these, its installation is challenge, mostly because:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the documentation is not exactly the most idiot-proof,&lt;/li&gt;
  &lt;li&gt;the workers do not behave like other works of, say, Mastodon,&lt;/li&gt;
  &lt;li&gt;load balancing is tricky.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, if you want to do something basic, &lt;a href=&quot;https://matrix.org/docs/older/understanding-synapse-hosting/&quot;&gt;the official documentation&lt;/a&gt; will get you going. But one someone installs something on a cluster, the main advantage is quick scalability. And that’s the thing there are few guidelines about on the internet.&lt;/p&gt;

&lt;p&gt;I’ll comment a few things about the last two things on that list, but first, one little note: if you don’t use Helm because, like me, you’re too lazy to learn it, do yourself a favor and learn how to use it. I got away by not using it to all the stuff I deployed (mastodon, pixelfed, bookwyrm, etc). But Matrix is a beast. It’s the kind of project you’d expect some help. I’ve seen people using &lt;a href=&quot;https://nix.dev/tutorials/nix-language.html&quot;&gt;Nix&lt;/a&gt; to maintain the deployment code, and if you speak Nix, lucky you. Someone I met on Matrix the other day send me &lt;a href=&quot;https://cgit.rory.gay/Rory-Open-Architecture.git/tree/host/Rory-nginx/services/matrix&quot;&gt;its setup&lt;/a&gt;, which is a work of art.&lt;/p&gt;

&lt;p&gt;But if you’re lazy like me, you will have a lot of yaml writing to get this done.&lt;/p&gt;

&lt;p&gt;So, get the configuration done by following the first steps of the abovementioned official documentation, and read along.&lt;/p&gt;

&lt;h4 id=&quot;the-workers&quot;&gt;The workers&lt;/h4&gt;

&lt;p&gt;Workers are the main unit when you want to scale up things. On kubernetes, a worker usually is a deployment, so one just scale up the number of replicas.&lt;/p&gt;

&lt;p&gt;It is a bit the same with Matrix, but with one caveat:&lt;/p&gt;

&lt;p&gt;Workers &lt;em&gt;need&lt;/em&gt; to have a unique name. Some of them, like the federation_sender’s, need to be explicitly referred to on the main configuration. So you cannot simply go ahead and increate the number of workers. Or, you can, provided that they can be configured with a unique name. There might be many good strategies to do so in Helm or even without it. Mine was this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; command: [&quot;/bin/sh&quot;, &quot;-c&quot;]
          args:
           - |
             sed  &quot;s/client_worker/${POD_NAME}/g&quot; /config/client.yaml &amp;gt; /tmp/client.yaml &amp;amp;&amp;amp;
             exec python -m synapse.app.generic_worker \
             --config-path=/data/homeserver.yaml \
             --config-path=/tmp/client.yaml
          ports:
            - containerPort: 8022
              name: http
            - containerPort: 9093
          volumeMounts:
            - name: client-config
              mountPath: /config
              readOnly: true
            - name: matrix-volume-claim
              mountPath: /data
      volumes:
        - name: client-config
          configMap:
            name: client-config
        - name: matrix-volume-claim
          persistentVolumeClaim:
            claimName: matrix-volume-claim
      restartPolicy: Always
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;This is how I use the image to deploy a generic worker of the type client. The config map looks like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;onfigmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: client-config
  namespace: matrix
data:
  client.yaml: |
    worker_name: client_worker
    worker_app: synapse.app.generic_worker
    worker_listeners:
      - type: http
        tls: false
        port: 8022
        resources:
          - names: [client,federation,media]
        bind_addresses: [&quot;0.0.0.0&quot;]
    redis:
      enabled: true
      host: 10.20.20.202
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So when scaling up, it will modify the ConfigMap when creating the pod. This is great for deployments of workers that don’t need to be on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;homeserver.yaml&lt;/code&gt;. If you need those, I suggest using StatefulSets, since their names are predictable, and add them to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;homeserver.yaml&lt;/code&gt;. Of course, unless you automate this, you need to adjust the amount of those workers on the main configuration manually if you increase or decrease them.&lt;/p&gt;

&lt;p&gt;Check the &lt;a href=&quot;https://element-hq.github.io/synapse/latest/workers.html&quot;&gt;documentation&lt;/a&gt; on workers. Some must be only a single instance, others can scale up independently (as long as their name are unique), and some needs a restart of all workers and a configuration change to work.&lt;/p&gt;

&lt;h4 id=&quot;load-balancing&quot;&gt;Load balancing&lt;/h4&gt;

&lt;p&gt;On most services, load balancing is done by redirecting traffic to some sort of web workers.&lt;/p&gt;

&lt;p&gt;On Matrix, it is more complicate than that. Load balancing here is done mostly by isolating some endpoints closely related to each other, like synchronization or federation activities, and sending them to respective workers.&lt;/p&gt;

&lt;p&gt;But besides that, two workers need more attention: federation inbound and sync.&lt;/p&gt;

&lt;p&gt;Sync is done in such a way that load balancing is optimal if it’s done by &lt;em&gt;user&lt;/em&gt;. The documentation has a few tricks on how to do that. On an nginx server, this is kinda trivial. But on the nginx ingress controller, this can be a bit more complicate.&lt;/p&gt;

&lt;p&gt;I went ahead and deployed a new ingress controller just for the matrix namespace on my cluster. That way, the rules I added won’t be applied to traffic to other services.&lt;/p&gt;

&lt;p&gt;I then added an http-snippet with the mappings mentioned on the documentation so I could get the variables to be used by nginx to do the load balancing:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;apiVersion: v1
data:
  allow-snippet-annotations: &quot;true&quot;
  annotations-risk-level: Critical
  http-snippet: |
     map $arg_since $sync {
     default &quot;matrix-sync-8022&quot;;
     '' &quot;matrix-sync-initial-8022&quot;;
     }
     map $arg_access_token $accesstoken_from_urlparam {
     default   $arg_access_token;
     &quot;~syt_(?&amp;lt;username&amp;gt;.*?)_.*&quot;           $username;
     }
     map $http_authorization $mxid_localpart {
     default                              $http_authorization;
     &quot;~Bearer syt_(?&amp;lt;username&amp;gt;.*?)_.*&quot;    $username;
     &quot;&quot;                                   $accesstoken_from_urlparam;
     }
  use-forwarded-headers: &quot;true&quot;
kind: ConfigMap
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: matrix-ingress
    app.kubernetes.io/name: matrix-ingress
    app.kubernetes.io/part-of: matrix-ingress
    app.kubernetes.io/version: 1.12.0-beta.0
  name: ingress-nginx-controller
  namespace: matrix
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You see that this requires predicting how nginx is going to refer to the services you create. Here, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;matrix-sync-2022&lt;/code&gt; is a deduction that the controller made by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;namespace&amp;gt;-&amp;lt;service name&amp;gt;-&amp;lt;portname&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That done, I used annotations on my ingress:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    nginx.ingress.kubernetes.io/upstream-hash-by: &quot;$mxid_localpart&quot;
    nginx.ingress.kubernetes.io/configuration-snippet: |
        if ($request_uri !~ &quot;^/_matrix/client/(unstable/org.matrix.simplified_msc3575|v5|v4|v3|r0|v1)/sync&quot; ) {
        set $proxy_upstream_name $sync;
        }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This allows:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;sending initial synchronizations, which are heavier, to particular workers;&lt;/li&gt;
  &lt;li&gt;load balancing things by the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The federation inbound is nicer with load balancing by ip, so you update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;upstream-hash-by&lt;/code&gt; with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$remoteaddr&lt;/code&gt; (I think and hope).&lt;/p&gt;

&lt;h4 id=&quot;other-things&quot;&gt;Other things&lt;/h4&gt;

&lt;p&gt;There are things that, as of today, are pretty much basic on Matrix, but the documentation doesn’t mention that you’ll want to install them, probably because they are still considered “Experimental”, though they are widespread, and new clients such as “Element X” rely on those.&lt;/p&gt;

&lt;p&gt;The first is &lt;a href=&quot;https://element-hq.github.io/matrix-authentication-service/setup/installation.html&quot;&gt;Matrix Authentication Service&lt;/a&gt;, the new authentication layer based on OIDC. You’ll want that, because that’s the future.&lt;/p&gt;

&lt;p&gt;The other is &lt;a href=&quot;https://github.com/element-hq/element-call&quot;&gt;Element Call&lt;/a&gt;. If you don’t have it, audio/video calls won’t work.&lt;/p&gt;

&lt;p&gt;The last thing is a turn sever, like &lt;a href=&quot;https://github.com/coturn/coturn&quot;&gt;Coturn&lt;/a&gt;, which is a TURN server to assist voip with other clients.&lt;/p&gt;

&lt;h4 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h4&gt;

&lt;p&gt;I really liked Matrix. It performs great if you don’t go to a room with 50.000 people. It’s installation, though, is very complex and granular. I heard that there are people that even create load balancing rules for a particular room, such as Matrix HQ, so that it won’t steal much processing.&lt;/p&gt;

&lt;p&gt;I hope this was useful for you. And thanks to the guys on the “Matrix on kubernetes” room on the Matrix space who helped a lot to understand a few of those things.&lt;/p&gt;

</description>
        <pubDate>Tue, 26 Nov 2024 21:39:23 +0100</pubDate>
        <link>https://francisaugusto.com/2024/Matrix-on-kubernetes/</link>
        <guid isPermaLink="true">https://francisaugusto.com/2024/Matrix-on-kubernetes/</guid>
        
        <category>kubernetes</category>
        
        <category>matrix</category>
        
        <category>synapse</category>
        
        <category>element</category>
        
        
        <category>personal</category>
        
      </item>
    
      <item>
        <title>Babb.no - The Fediverse and open social networks</title>
        <description>&lt;h4 id=&quot;introduction&quot;&gt;Introduction&lt;/h4&gt;

&lt;p&gt;TL;DR: &lt;a href=&quot;https://about.babb.no&quot;&gt;About babb.no&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I really want to stop using social network in this age of algorithms, monetization of personal data and AI. I like when I am in control of my own data, where I can migrate my data wherever I want.&lt;/p&gt;

&lt;p&gt;Last year, I got to know about &lt;a href=&quot;https://joinmastodon.org&quot;&gt;Mastodon&lt;/a&gt;, a microblogging platform that gained fame just after Elon Musk bought Twitter. I decided that Mastodon was something for me, so I started &lt;a href=&quot;https://mastodon.babb.no&quot;&gt;Mastodon.babb.no&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then I discovered &lt;a href=&quot;https://pixelfed.org&quot;&gt;Pixelfed&lt;/a&gt;. And then &lt;a href=&quot;https://friendi.ca&quot;&gt;Friendica&lt;/a&gt;. And finally &lt;a href=&quot;https://joinbookwyrm.com&quot;&gt;Bookwyrm&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The cool thing about these sites is that:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;There is no algorithm scraping my data and offering me ads to buy stuff;&lt;/li&gt;
  &lt;li&gt;no hidden use of my data and pictures;&lt;/li&gt;
  &lt;li&gt;I can focus on content rather than skipping through useless ads or content I don’t want to see,&lt;/li&gt;
  &lt;li&gt;The API’s are open, so I can do a lot of things.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sites above work sort of an alternative to known services:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Mastodon is an alternative for Twitter/X/Threads;&lt;/li&gt;
  &lt;li&gt;Pixelfed is an alternative for Instagram;&lt;/li&gt;
  &lt;li&gt;Friendica is an anternative for Facebook;&lt;/li&gt;
  &lt;li&gt;Bookwyrm is an alternative for Goodreads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cool thing is that if you have a user on one of these sites, you can follow anyone on the other instances. So if I have a user on Mastodon, I can follow users on Pixelfed. And the otherway around. This is thanks to the ActivePub protocol.&lt;/p&gt;

&lt;p&gt;They are all fully functional services, though somethings are not there yet, the most notable ones being Friendica and Bookwyrm not having an iOS app (or an app at all, like Bookwyrm).&lt;/p&gt;

&lt;p&gt;I decided to self host these websites. I could have joined other instances, but I believe it’s good to give something back.&lt;/p&gt;

&lt;p&gt;My instances are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Mastodon: &lt;a href=&quot;https://mastodon.babb.no&quot;&gt;mastodon.babb.no&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Pixelfed: &lt;a href=&quot;https://pixelfed.babb.no&quot;&gt;pixelfed.babb.no&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Friendica: &lt;a href=&quot;https://social.babb.no&quot;&gt;social.babb.no&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Boomwyrm: &lt;a href=&quot;https://bookwyrm.babb.no&quot;&gt;books.babb.no&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;nerdy-talk&quot;&gt;Nerdy talk&lt;/h3&gt;

&lt;p&gt;They were all relatively easy to install, but Bookwyrm was a bit harder as the installation was really based on Docker-compose. Since I run a kubernetes cluster, I had to adapt things a bit. I posted the whole thing on &lt;a href=&quot;https://github.com/oculos/bookwyrm-kubernetes&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;I hope that someday we will all use social media that is community-driven, I really like social media, not less because I need a way to keep in touch with friends and family who live far. The challenge today is to get people to migrate to these opensources solutions. But now the stepping stones are in place.&lt;/p&gt;

</description>
        <pubDate>Sun, 17 Dec 2023 12:39:23 +0100</pubDate>
        <link>https://francisaugusto.com/2023/Babb-no/</link>
        <guid isPermaLink="true">https://francisaugusto.com/2023/Babb-no/</guid>
        
        <category>fediverse</category>
        
        <category>mastodon</category>
        
        <category>bookwyrm</category>
        
        <category>pixelfed</category>
        
        <category>friendica</category>
        
        
        <category>personal</category>
        
      </item>
    
      <item>
        <title>Running a Mastodon instance on kubernetes</title>
        <description>&lt;h4 id=&quot;introduction&quot;&gt;Introduction&lt;/h4&gt;

&lt;p&gt;I heard about &lt;a href=&quot;https://joinmastodon.org&quot;&gt;Mastodon&lt;/a&gt; when it got widely mentioned as a result of Elon Musk’s Twitter acquisition. Its goal really got me interested, and I fell in love about it from day one.&lt;/p&gt;

&lt;p&gt;I decided to run my own instance, &lt;a href=&quot;https://mastodon.babb.be&quot;&gt;Mastodon.babb.be&lt;/a&gt;, and it has been a joy to use it and to maintain it.&lt;/p&gt;

&lt;p&gt;I want users to come, but when I heard other stories of servers having to quickly scale up in order to accomodate the huge number of users coming from Twitter, I decided to get ready for such influx should it happen.&lt;/p&gt;

&lt;p&gt;My goal was to setup a kubernetes cluster for my static websites, as well as for my old WordPress-based blog. The cluster was installed, everything seems to be running smooth (despite Ubuntu 22.04 quirks with kubernetes), so it was time to get Mastodon moved to it.&lt;/p&gt;

&lt;h3 id=&quot;the-challenge&quot;&gt;The challenge&lt;/h3&gt;

&lt;p&gt;There were few recipes on how to move Mastodon to kubernetes.&lt;/p&gt;

&lt;p&gt;Mastodon comes with a Helm chart, but I really didn’t want to use it - some people out there say it is quite old. I also saw some other kubernetes deployments, but they were from more than four years ago, and a lot has changed.&lt;/p&gt;

&lt;h2 id=&quot;how&quot;&gt;How&lt;/h2&gt;

&lt;p&gt;I had already a running, non containerized instance. So I had already a configured setup, which made things easier for me.&lt;/p&gt;

&lt;p&gt;Initially, I used &lt;a href=&quot;https://kompose.io&quot;&gt;Kompose&lt;/a&gt; to convert the docker-compose.yaml supplied by Mastodon’s &lt;a href=&quot;https://github.com/mastodon/mastodon&quot;&gt;repo&lt;/a&gt; to kubernetes deployments, services, etc.&lt;/p&gt;

&lt;p&gt;After that, some cleaning up and adaptation, I did :&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;throw out some files, like network configuration, redis, database and postgresql deployments, etc, as I run most of the persistent stuff outside my cluster.&lt;/li&gt;
  &lt;li&gt;create several deployments for the sidekiq queues, as suggested by &lt;a href=&quot;https://nora.codes/post/scaling-mastodon-in-the-face-of-an-exodus/#fn:1&quot;&gt;Nora&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;move sensitive key/values out of the env.production configMap to a secrets file&lt;/li&gt;
  &lt;li&gt;created an ingress for web and another for streaming. I probably overdid it a bit, as I have a reverse proxy outside my cluster, so I moved some of nginx configuration from mastodon to my ingress, just to be on the safe side.&lt;/li&gt;
  &lt;li&gt;finally, just for the kicks, I added a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;topologySpreadConstraints&lt;/code&gt; to my deployments to make sure the pods would be allocated evenly on my cluster (I got this one &lt;a href=&quot;https://medium.com/geekculture/kubernetes-distributing-pods-evenly-across-cluster-c6bdc9b49699&quot;&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I applied all the yaml and my cluster was then online.&lt;/p&gt;

&lt;h2 id=&quot;remained-things-to-do&quot;&gt;Remained things to do:&lt;/h2&gt;

&lt;p&gt;Remember the cronjobs? Well, we need those. I need to create a CronJob resource on my cluster, but, for now, what I did was that I stopped all my mastodon services on my original instance, but kept the cronjobs there. I’ll remove those eventually and create CronJob instances on the cluster.&lt;/p&gt;

&lt;p&gt;Please give me any comments or feedback. All the files I ended up using can be found on my &lt;a href=&quot;https://github.com/oculos/mastodon-k8s&quot;&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;
</description>
        <pubDate>Fri, 09 Dec 2022 15:20:23 +0100</pubDate>
        <link>https://francisaugusto.com/2022/Mastodon-on-a-k8s-kluster/</link>
        <guid isPermaLink="true">https://francisaugusto.com/2022/Mastodon-on-a-k8s-kluster/</guid>
        
        <category>mastodon</category>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        
        <category>personal</category>
        
      </item>
    
      <item>
        <title>Booting a Raspberry Pi 400 from FreeNAS with Unifi</title>
        <description>&lt;h4 id=&quot;introduction&quot;&gt;Introduction&lt;/h4&gt;

&lt;p&gt;I am a hopeless collector of Raspberry Pi’s: they come out, I try to grab them. And when I saw the Raspberry Pi 400, being the nostalgic ex-Amiga user that I am, I knew I just had to buy one. My little daughter sometimes asks me to boot the Pi with RetroPie, so that she can play Road Warrior, an old Nintendo game that I used to play when I was a kid.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../assets/2021/R400.jpg&quot; alt=&quot;The cute Raspberry Pi 400!&quot; title=&quot;The cute Raspberry Pi 400!&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Swapping those SD cards is annoying, though. And having lost a lot of work on SD cards before has made me not trust the Pi as much when I need something to be a bit more permanent.&lt;/p&gt;

&lt;p&gt;So I read that it is easy to get the Raspberry Pi booting off the network, and I decided it was time to try it.&lt;/p&gt;

&lt;p&gt;I followed these two guides:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://hackaday.com/2019/11/11/network-booting-the-pi-4/&quot;&gt;Network booting the Pi 4&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.virtuallyghetto.com/2020/07/two-methods-to-network-boot-raspberry-pi-4.html&quot;&gt;Two methods to network boot Raspberry Pi 4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It worked fine, with two caveats:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;My Pi would not get the TFTP address from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dnsmasq&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;NSF 4 didn’t work for me (and it still doesn’t)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I use TrueNAS at home, and I have a Unifi Security Gateway, which allow me to configure some DHCP options. Doing that made my life a bit easier, actually, in that I only had to configure the NFS share and the TFTP service, and things worked.&lt;/p&gt;

&lt;p&gt;So I’ll try to sum up how I got this working, without &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dnsmasq&lt;/code&gt; and using my TrueNAS.&lt;/p&gt;

&lt;h4 id=&quot;goal&quot;&gt;Goal&lt;/h4&gt;

&lt;p&gt;My goal is to boot by Raspberry Pi 400 off my network. I want to boot RetroPie, since the obvious advantage is that I’d have even more space to roms if I need to. And this way, if I connect another Raspberry Pi to my TV, I could boot off the same share (if you do that, notice that you shouldn’t use both systems simultaneously)&lt;/p&gt;

&lt;h4 id=&quot;list-of-used-equipmentsoftware&quot;&gt;List of used equipment/software&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;Raspberry Pi 400 (should work with a Raspberry Pi 4)&lt;/li&gt;
  &lt;li&gt;An SD card with RetroPie installed on it&lt;/li&gt;
  &lt;li&gt;FreeNAS/TrueNAS (for the NFS and TFTP services - you can use other servers, of course)&lt;/li&gt;
  &lt;li&gt;Unifi Security Gateway (should work with other DHCP servers - YMMV).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;creating-the-shares-at-the-freenas&quot;&gt;Creating the shares at the FreeNAS&lt;/h4&gt;

&lt;p&gt;First, we need to create the directories where we are going to put the files the Pi will boot off:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Create a new dataset on your pool. Give it a name (let’s say, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pi&lt;/code&gt;). Accept the default options. So if your pool is called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mypool&lt;/code&gt;, you will end up with a folder called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/mnt/mypool/pi&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the shell, do this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ mkdir -p /mnt/mypool/pi/boot
$ chmod 777 /mnt/mypool/pi/boot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;Note that this is assuming I will not boot other Pi’s with different OS’s. If you want to do that, it is probably best to create a directory with the serial number of the Pi you want to boot. See Step 6 &lt;a href=&quot;https://www.virtuallyghetto.com/2020/07/two-methods-to-network-boot-raspberry-pi-4.html&quot;&gt;here&lt;/a&gt;.
The Pi first looks for its files on the remote computer on a folder with its serial number. If it doesn´t find one, it looks for the root folder. I am using this latter approach.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, we are going to configure the TFTP service, which is used by the Pi to load the OS:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;On the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Services&lt;/code&gt; menu of your FreeNAS/TrueNAS, enable TFTP, and edit it.&lt;/li&gt;
  &lt;li&gt;Choose the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/mnt/mypool/pi/boot&lt;/code&gt; as the folder to be shared by the TFTP service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then we have to configure the NFS share.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;On the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sharing&lt;/code&gt; menu, choose  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Unix shares (NFS)&lt;/code&gt;, and click on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Add&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;On Path, choose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/mnt/mypool/pi&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Click &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;All dirs&lt;/code&gt;, and then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Advanced options&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;On &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Maproot user&lt;/code&gt;, choose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;root&lt;/code&gt;, and on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Maproot group&lt;/code&gt;, choose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wheel&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Choose the network and/or hosts that will be allowed to mount your share&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;Note that security-wise these might not be the best permissions. Proceed with caution!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There! Now your FreeNAS is configured to serve your Pi! It needs the files, though…&lt;/p&gt;

&lt;h4 id=&quot;transferring-the-files-from-the-pi&quot;&gt;Transferring the files from the Pi&lt;/h4&gt;

&lt;p&gt;The tutorials I referred to at the beginning of this article are based on an installation of a fresh image of an OS (Raspbian). I would rather copy an existing OS that I have installed on an SD card.&lt;/p&gt;

&lt;p&gt;To do that, I had to copy the contents of the two partitions of the SD card - the root and the boot partitions.&lt;/p&gt;

&lt;p&gt;I use a Mac, so the root partition is not automatically readable on macOS. But if you are using an Intel-based mac, you can do this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ brew install cask osxfuse
$ brew install ext4fuse
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This way, you can mount the root partition of the SD card after inserting it on your SD card reader:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;disklist list # check what's the name of the linux partition of the SD card - for example, disk2s2
sudo ext4fuse /dev/disk2s2 /tmp/raspberry
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;blockquote&gt;
  &lt;p&gt;Caveat: This method has the problem that the directories are mounted as read-only. I changed a few (such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/home&lt;/code&gt;) recursively so that they could be writeable again after copying. If you have access to a Linux machine, you’re better off just copying the files from there instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If all works fine, you now have the files you need. Let’s copy the boot folder to the FreeeNAS:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ scp -r /Volumes/boot/* myuser@myfreenasaddress:/mnt/mypool/pi/boot/.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;However, due to the incompatibility of some filenames between Mac and Linux, we have to do an extra step to copy the root partition:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;brew install gnu-tar
sudo gtar czf rasp.tgz /tmp/raspberry
scp rasp.tgz myuser@myfreenasaddress:/mnt/mypool/.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, on your FreeNAS:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cd /mnt/mypool
tar -xzf rasp.tgz
mv raspberry/* pi/* 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;adjustments-on-the-configurations-for-booting&quot;&gt;Adjustments on the configurations for booting&lt;/h4&gt;

&lt;p&gt;Good? Good. We just need to substitute two files on your FreeNAS:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cd /mnt/mypool/pi/boot
rm start4.elf
rm fixup4.dat
wget https://github.com/Hexxeh/rpi-firmware/raw/stable/start4.elf 
wget https://github.com/Hexxeh/rpi-firmware/raw/stable/fixup4.dat 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Somehow, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/home/pi&lt;/code&gt; folder got the wrong permissions set when copying the files. I believe it is because, on a Mac, ext4 partitions are read-only (when mounting with ext4fuse), so that’s probably why it happened. So let’s fix this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ chmod -R 750 /mnt/mypool/pi/home/pi
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, we going to configure the pi to mount your nfs share. Edit the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmdline.txt&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ nano /mnt/mypool/pi/boot/cmdline.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Erase the existing configuration andcopy this one:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;console=serial0,115200 console=tty root=/dev/nfs nfsroot=192.168.1.110:/mnt/mypool/pi,vers=3 rw ip=dhcp rootwait elevator=deadline
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;There are few other configurations from the RetroPie install on this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmdline.txt&lt;/code&gt; file, such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;consoleblank=0&lt;/code&gt;. This one, for example, is safe to keep, though I also removed it so I can see if there are errors while booting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And finally, edit the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/mnt/mypool/pi/etc/fstab&lt;/code&gt; and remove the two lines you see there with UUID mountings.&lt;/p&gt;

&lt;p&gt;There! You got the FreeNAS all configured to serve your files. Now, let’s get your Pi to boot off the network.&lt;/p&gt;

&lt;h4 id=&quot;configuring-the-pi-firmware&quot;&gt;Configuring the Pi firmware&lt;/h4&gt;

&lt;p&gt;In the tutorials I mentioned, you need to download a newer firmware to your Raspberry Pi 4 to get things working, but most Pi’s now have a newer firmware and thus are able to boot off the network. You need to configure it, though. Boot your Pi with an existing Raspbian installed, and type&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ rpi-eeprom-config --edit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Add this option:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;BOOT_ORDER=0x21
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will make the Pi to attempt booting off the SD card and, if it isn’t present, it will then try to boot off the ethernet. You have other boot options. For more information, check the &lt;a href=&quot;https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711_bootloader_config.md&quot;&gt;documentation of the bootloader&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, we can configure something else: if you don’t want to install dnsmasq, or don’t want to configure your DHCP server to send the FreeNAS (TFTP) address to your pi on boot, you can add this configuration as well:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;TFTP_IP=192.168.1.110
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Change the above IP address to the one corresponding to your FreeNAS. If you choose to add this, you are all set, actually. Just save the configuration, reboot, then reboot again, this time without your SD card.&lt;/p&gt;

&lt;p&gt;If you’d rather have your DHCP server to send the TFTP address to your Pi, read on.&lt;/p&gt;

&lt;h4 id=&quot;configuring-your-unifi-dhcp-to-send-tftp-information&quot;&gt;Configuring your Unifi DHCP to send TFTP information&lt;/h4&gt;

&lt;p&gt;Open your Unifi Controller on your browser, and go to the Networks session on the Settings menu.
Choose the network where your Pi will be configured (for example LAN), and click on “Advanced DHCP options”. There, enter the address of your FreeNAS on the DHCP TFTP Server field:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../assets/2021/dhcp.jpg&quot; alt=&quot;DHCP configuration&quot; title=&quot;DHCP configuration&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Save it, and bang, you are done. Now you will be able to boot your Pi off your RetroPie! I have used a bit now, and it is working as it should. I didn’t manage to upgrade it, though, probably due some permission issues, so I boot off the SD card again, did a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo apt update&lt;/code&gt; and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo apt dist-upgrade&lt;/code&gt; and redid the copying of the files to the NAS. The best is if you install RetroPie from the scratch, using the instructions on the tutorials I mentioned (they also apply to the RetroPie, as it is Raspbian-based).&lt;/p&gt;

&lt;h4 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h4&gt;

&lt;p&gt;I really enjoyed this. I didn’t seem to get a much speedier boot, since the Raspberry 4 already reads the SD card at a higher speed. But the flexibility of dual booting, and also of being able have much more storage for my RetroPie, made this really worth it.&lt;/p&gt;

&lt;p&gt;I really hope that someone comes up with a tutorial on how to get a PXE-menu working. Probably using the &lt;a href=&quot;https://rpi4-uefi.dev&quot;&gt;UEFI firmware&lt;/a&gt; would help here, but I am not sure my OS’es would boot when UEFI is loaded, and it seems it also needs to boot the UEFI from an SD card, which is something I really want to avoid.&lt;/p&gt;

</description>
        <pubDate>Sat, 27 Mar 2021 12:20:23 +0100</pubDate>
        <link>https://francisaugusto.com/2021/Booting-Pi-From-FreeNAS/</link>
        <guid isPermaLink="true">https://francisaugusto.com/2021/Booting-Pi-From-FreeNAS/</guid>
        
        <category>freenas</category>
        
        <category>truenas</category>
        
        <category>raspberrypi</category>
        
        
        <category>personal</category>
        
      </item>
    
      <item>
        <title>Fiber at home - an adventure</title>
        <description>&lt;h4 id=&quot;a-little-background&quot;&gt;A little background&lt;/h4&gt;

&lt;p&gt;I am lucky to be inspired by very smart people at work, all the time.&lt;/p&gt;

&lt;p&gt;Almost three years ago, I wanted to buy a NAS (Network Attached Storage), as having hard isks connected to a router’s USB port wasn’t cutting it anymore. I had my eyes on a QNAP when a colleague came in with an older HP desktop and said “hey, try FreeNAS on this one, just to make sure a NAS is your thing”. Little did I know that it was the start of a lot of work, learning and joy (some of it described on my post about &lt;a href=&quot;https://francisaugusto.com/2019/Building-a-Supermicro-based-Freenas/&quot;&gt;building my homelab machine&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;It turned out that the HP desktop was too loud, not something I could keep on our living room. So I was &lt;del&gt;ordered&lt;/del&gt; asked politely to move it to the basement. Luckily my wifi still could reach the basement, but a NAS on WiFi is not a thing, really. So I was not able to play with the cool stuff like Nextcloud when the speeds were that bad.&lt;/p&gt;

&lt;p&gt;I then applied to the board of our housing association to be able to pull a cable from my flat to the basement. I got an approval, but under the condition that I had to find an existing duct to pull the cable. I wasn’t allowed to drill or otherwise make a new duct. “Piece of cake”, I thought, not knowing it was going to take me almost 2 years to get it done.&lt;/p&gt;

&lt;p&gt;Three electricians came here, plus a couple of smart friends. Nobody saw anything usable. We have old ducts used for antennas, but they go upwards, and while we also have a storage room on the loft, we have no power there to plug stuff. I thought I could use the same duct used for the cable TV, but it turns out that one also goes up to the loft.&lt;/p&gt;

&lt;p&gt;I was frustrated, as I really wanted to use my NAS. I focused then on building a silent machine that I could have on the living room, and that I managed to do, as described on the link above.&lt;/p&gt;

&lt;p&gt;But with the pandemics came home office, and I have to work on the living room. That low humming of the hard disks (which is the only noise coming out from that machine) was really annoying me. I had to fix it.&lt;/p&gt;

&lt;p&gt;I then asked permission to the board to get power on the loft, since all the ducts seemed to be going there. They gave me the permission - amazing board! - but recommended me to wait until august (this was like may 2020) since a company was going to upgrade the lights and electricity of the whole building, so it would make sense to get this done by the same company. Deal.&lt;/p&gt;

&lt;p&gt;Things got postponed, and the company started its work on our building only in December. But, as they say, good things come to those who wait: I decided to use fiber optics instead of an ethernet (cat6/7) cable, as my motherboard have SFP+ ports, and I thought it was more future proof. It turns out that the company had an employee who worked in telecom for decades and could do the job for me.&lt;/p&gt;

&lt;h4 id=&quot;the-odyssey-to-get-fiber-spliced&quot;&gt;The odyssey to get fiber spliced&lt;/h4&gt;

&lt;p&gt;The electrician came to see the flat, and to check the loft to see how to get power there. He thought it would be hard to use the duct used for tv cables, as it seemed to be tight there. And then he asked, “why don’t you use the cellar?”. I explained to him that I didn’t find any existing duct I could use to pass the cable through to the cellar.&lt;/p&gt;

&lt;p&gt;While we took the stairs down, he immediatelly looked about my entry door and asked, “what is this?”. That was a little box between the doors of the two flats. The man worked in telecom for decades, as I said, so he knows his stuff. He took some stairs, opened the little box, and bingo! He found a duct to the cellar, which was not in use and had just old telephone cables there.&lt;/p&gt;

&lt;p&gt;This is the old phone duct:
&lt;img src=&quot;../../assets/2021/telephoneduct2.jpg&quot; alt=&quot;The old phone duct!&quot; title=&quot;The old phone duct!&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We agreed then we would do the job in January 2021, as it seemed to be possible to pass the fiber cable through that duct.&lt;/p&gt;

&lt;p&gt;But then my luck went way: his company couldn’t find the appropriate cable (I wanted OM4, as I wanted to use multi mode fiber, which I kinda regret now, but I already had the multi mode transceivers). I decided to buy the cable myself.&lt;/p&gt;

&lt;p&gt;If you try to buy anything related to fiber optics in Norway as a private customer (ie. not as a company), you are basically out of luck. But finally I found a company that accepted to sell me the cable I wanted, and I had to take a 40 min ride on a bus to go there, walk on slippery roads and get the cable home. I called the guy to ask when we could start pulling the cable, but it turns out he was on a sick leave and wasn’t going to work before a few weeks. I was on my own.&lt;/p&gt;

&lt;p&gt;I asked the company where I bought the cable if they could recommended me of anyone that could do the job for me, and they gave me a name. I was skeptical, because EVERY single company I called either didn’t reply or wouldn’t take my business. But this guy agreed to come and see my building.&lt;/p&gt;

&lt;p&gt;Then we got into another problem: the duct ends up on a box on the basement that nobody had a key for.&lt;/p&gt;

&lt;p&gt;The guy, as the one previously, said he might have a key for it (it is an old box that was used back in the days by Telenor - the Norwegian telecom incumbent). Suddenly the guy disappeared! He wouldn’t reply my sms’s, and I was on my own again.&lt;/p&gt;

&lt;p&gt;I had no idea how to open this box:
&lt;img src=&quot;../../assets/2021/thebox.jpeg&quot; alt=&quot;Who can open this box?&quot; title=&quot;Who can open this box?&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Another colleague recommended an electrician, but this one wouldn’t be available before two months. He had a colleague that would call me instead, but never did.&lt;/p&gt;

&lt;p&gt;Fortunately, my colleagues are not just a source of inspiration, they helped me a lot. The same one who encouraged me to start with FreeNAS offered to see if we could pull the cable ourselves. That way, I would save a lot with the costs.&lt;/p&gt;

&lt;p&gt;It was tough: very tight ducts, lots of old wires. Still, we were not sure we could make it through the box. I bought a fish tape and an endoscopic camera to see if we could avoid opening the box, but that was a no go.&lt;/p&gt;

&lt;p&gt;Now I had to ask the board permission to ask a locksmith to open the box and change the key so we could open it. I was really relieved when I got the permission to do it. I called a locksmith, and after a few days he came and opened the thing.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../assets/2021/openbox.jpeg&quot; alt=&quot;It's open!&quot; title=&quot;It's open!&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Now my friend came home to help me, and we tried to pull the cable through the duct, and for a few minutes we thought it wasn´t going to go through, but after a few attempts, we finally reached the cellar!&lt;/p&gt;

&lt;p&gt;We then pulled the fiber cable with the help of the fish tape, and it reached my storage room! Perfect! Now I need to get this fiber spliced!&lt;/p&gt;

&lt;p&gt;Oh, the joy to see that cable come through…&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../assets/2021/fiberarrived.jpeg&quot; alt=&quot;Oh, the joy...&quot; title=&quot;Oh, the joy...&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The thing is that I bought the cable terminated on one side, but it had to be terminated on the other side, as it would never pass the ducts with the connectors.&lt;/p&gt;

&lt;p&gt;My saga with companies specialized in fiber optics restarted: no company would splice the fibers for me, and those who had splicing machines for rent would not rent them to me (they said they only rent to other companies, not to private customers). I was getting desperate.&lt;/p&gt;

&lt;p&gt;I decided then to press the “F..it” button and buy a cheaper (which is still expensive) fiber splicing machine. Saw some on Ali Express and Banggood, and decided to get the same model from Amazon, as the delivery was faster and because, well, I trust Amazon a bit more than individual sellers on Ali Express.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../../assets/2021/Basement.jpeg&quot; alt=&quot;The basement was ready for it!&quot; title=&quot;The basement was ready for it!&quot; /&gt;&lt;/p&gt;

&lt;p&gt;My plan was to buy the machine, splice my cable, make sure it works, and then sell the machine. I’ve put it on the shopping cart on Amazon, went to the kitchen to take the bread I was baking out of the oven, and when I was coming back to finalize the shopping, I got a call from the electrician who was on a sick leave! He was back, happy that I managed to pass the cable, and he could come in two days to splice the fiber! I can’t tell you what a relief that was.&lt;/p&gt;

&lt;p&gt;He came last Friday, spliced the fibers (I got two pairs), and had everything beautifully installed down the basement. He asked me to test, and to tell him if anything was wrong.&lt;/p&gt;

&lt;p&gt;When he left, I tested, it didn’t work. Tested again, no signal. On the third time, after I inverted the connects, it worked! I got now one (possibly two, I haven’t tested the other fiber pair yet) 10Gbits/sec connection to my basement!&lt;/p&gt;

&lt;p&gt;I got my big NAS box out of our living room, connected to the switch in the basement, and came running upstairs to see if everything was ok. Ah, the joy of a quiet living room…&lt;/p&gt;

&lt;h4 id=&quot;aftermath&quot;&gt;Aftermath&lt;/h4&gt;

&lt;p&gt;I am pretty happy with my setup. It was totally worth it. Oslo is a very expensive city, so space use has to be optimized. Being able to use my storage room to house some equipment is perfect: cool temperatures, no problems with noise, and room to experiment a bit with other equipment if I want to do so.&lt;/p&gt;

&lt;p&gt;However, the bitter taste is still here: it shouldn’t have to be this hard. It is appalling to think how hard it is to get a bit of a slightly more specialized equipment when you are not a company. I could have ordered a lot from overseas, which I did in fact - &lt;a href=&quot;https://fs.com&quot;&gt;fs.com&lt;/a&gt; has an amazing service and sold me some of the patch cables I needed. But it is shocking that if a user wants to install fiber for using on an internal LAN, it might be very hard to buy the components or the get someone to do the fiber service. A part of me wishes that I should have bought that machine, so that I won’t have to rely on the good will of companies to help me if I need to service the cable, replace it, etc.&lt;/p&gt;
</description>
        <pubDate>Sat, 06 Mar 2021 15:20:23 +0100</pubDate>
        <link>https://francisaugusto.com/2021/Fiber-as-cat6-alternative-an-adventure/</link>
        <guid isPermaLink="true">https://francisaugusto.com/2021/Fiber-as-cat6-alternative-an-adventure/</guid>
        
        <category>fiber</category>
        
        <category>freenas</category>
        
        <category>truenas</category>
        
        <category>esxi</category>
        
        <category>supermicro</category>
        
        
        <category>personal</category>
        
      </item>
    
      <item>
        <title>Configuring a Linux image for Horizon instant clones, including Active Directory and NFSv4</title>
        <description>&lt;p&gt;A pool of instant clones of VDI (Virtual Desktop Interface) is the one where an image of an OS is cloned so that several users can login to a similar computer set-up. Normally, these clones are destroyed and recreated, so it is common to setup an strategy for persistent storage.&lt;/p&gt;

&lt;p&gt;VMware has provided some documentation on how to configure Linux machines for those pools of instant clones, and arguably the most challenging part is the integration with Active Directory. Most of the documentation is linked from a document called &lt;a href=&quot;https://docs.vmware.com/en/VMware-Horizon-7/7.8/linux-desktops-setup/GUID-D8E3A4AA-83E9-46A4-8BBA-824027146E93.html#GUID-D8E3A4AA-83E9-46A4-8BBA-824027146E93&quot;&gt;Integrating Linux with Active Directory&lt;/a&gt;. However, I’ve found that the documentation provided by VMWare can be a bit outdated or incomplete.&lt;/p&gt;

&lt;p&gt;One example: On &lt;a href=&quot;https://docs.vmware.com/en/VMware-Horizon-7/7.8/linux-desktops-setup/GUID-986977D4-87CE-459C-BC2A-55C0B6EA09AC.html&quot;&gt;this document&lt;/a&gt;, VMware doesn’t mention that the clones need to rejoin the domain when created, though on another &lt;a href=&quot;https://docs.vmware.com/en/VMware-Horizon-7/7.7/linux-desktops-setup/GUID-EA063015-63BB-44AE-BF66-D3ED2F1ABFF0.html&quot;&gt;document describing the procedure for RHEL&lt;/a&gt; this step is indeed mentioned.&lt;/p&gt;

&lt;p&gt;Another problem is that VMware skips the simplest of the situations, which is that AD can simply be joined by configurind SSSD correctly using ad as id_provider, auth_provider and access_provider, and not necessarily having to use &lt;a href=&quot;https://docs.vmware.com/en/VMware-Horizon-7/7.8/linux-desktops-setup/GUID-524AE8EE-1084-4F1B-A6B0-553DABA06087.html&quot;&gt;Winbind&lt;/a&gt;, &lt;a href=&quot;https://docs.vmware.com/en/VMware-Horizon-7/7.8/linux-desktops-setup/GUID-986977D4-87CE-459C-BC2A-55C0B6EA09AC.html&quot;&gt;Samba&lt;/a&gt;, &lt;a href=&quot;https://docs.vmware.com/en/VMware-Horizon-7/7.8/linux-desktops-setup/GUID-1E715FE3-0C00-45FC-B395-05D12E5D9E1A.html&quot;&gt;OpenLDAP&lt;/a&gt; or &lt;a href=&quot;https://docs.vmware.com/en/VMware-Horizon-7/7.8/linux-desktops-setup/GUID-1E715FE3-0C00-45FC-B395-05D12E5D9E1A.html&quot;&gt;LDAP authentication&lt;/a&gt; to achieve this. In other words, you can still join AD without needing to use Samba, Winbind or LDAP, so there is a way to join AD in a more straightforward way.&lt;/p&gt;

&lt;p&gt;This document attempts to supplement the information found elsewhere on how to properly configure a Linux OS image for deployment of an instant clones-pool. I used RHEL 8 here, but most of the things I mention can be used on other distros, though RHEL 8 is supported officially by Horizon 2006.&lt;/p&gt;

&lt;h3 id=&quot;summary-of-the-steps-you-need-and-what-isnt-covered-here&quot;&gt;Summary of the steps you need and what isn’t covered here&lt;/h3&gt;

&lt;p&gt;I’m going to explain the following steps, which is what you need to configure your Linux image in order to get instant clones:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Join the base (called golden image on VMWare documentation) to Active Directory&lt;/li&gt;
  &lt;li&gt;Optionally configure &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pam_mount&lt;/code&gt; so that your users get home directories mounted from an NFSv4 server&lt;/li&gt;
  &lt;li&gt;Make a decision on how you are going to save your credentials on the gold image so that the cloned images can (re)join the domain&lt;/li&gt;
  &lt;li&gt;Write a script to orchestrate the rejoining&lt;/li&gt;
  &lt;li&gt;Change the Horizon View agent configuration to run that script you created for rejoining&lt;/li&gt;
  &lt;li&gt;Shutdown the image, create a snapshot if it doesn’t have one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these steps, you can deploy your image.&lt;/p&gt;

&lt;p&gt;Things I don’t mention here but I assume you have done them already:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Create a user on AD for the purpose of joining other machines to AD&lt;/li&gt;
  &lt;li&gt;Create the configuration for kerberos on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/krb5.conf&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Install the Horizon View Agent for Linux&lt;/li&gt;
  &lt;li&gt;DNS is working fine, the hosts resolve to the domain you’re using, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ready? Let’s do it!&lt;/p&gt;

&lt;h3 id=&quot;setting-up-the-image&quot;&gt;Setting up the image&lt;/h3&gt;

&lt;h4 id=&quot;configuring-your-etcsssdsssdconf&quot;&gt;Configuring your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/sssd/sssd.conf&lt;/code&gt;&lt;/h4&gt;

&lt;p&gt;As I said, VMware does not mention using AD as a source of authentication. Configure this by editing your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/sssd/sssd.conf&lt;/code&gt;. Here is an example of a working configuration:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[sssd]
config_file_version = 2
domains = mydomain.com
services = nss, pam, pac, sudo

[nss]
allowed_shells = *
default_shell = /bin/bash
shell_fallback = /bin/bash
homedir_substring = /home
override_homedir = %H/%u

filter_groups = root,apache,mysql,postgres,store,palantir,palconf,mailman
filter_users = root,apache,mysql,postgres,store,palantir,mailman,ghost,priss

[domain/EXAMPLE.COM]
id_provider = ad
auth_provider = ad
access_provider = ad
autofs_provider = ad
chpass_provider = ad
ldap_id_mapping = false # You might want to set this to true, but if you have uidNumber and gidNumber on your users' records, it works well with false.
enumerate = false
dns_discovery_domain = example.com

krb5_realm = EXAMPLE.COM
krb5_renewable_lifetime = 14d
krb5_renew_interval = 3600
krb5_ccname_template = FILE:%d/krb5cc_%U
ad_gpo_map_interactive = +gdm-vmwcred
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Not all settings above are mandatory, but those are the ones that worked for us.&lt;/p&gt;

&lt;h4 id=&quot;join-the-domain&quot;&gt;Join the domain&lt;/h4&gt;

&lt;p&gt;You can join the domain on a RHEL 8 install by using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;realm join&lt;/code&gt;, but it configures a lot of the things which we already configured by hand on the sssd file. If you perform a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;realm leave&lt;/code&gt;, it ends up erasing your configuration, so I decided to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;adcli&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But before joining the domain, it’s important to decide if, when rejoining the domain on the clones, you are going to save the credentials or if you are going to use the password. Both have its advantages and disadvantages.&lt;/p&gt;

&lt;p&gt;Using the password can be interesting if you don’t plan to build your images too often. However, it is less secure, especially if the user you use to join AD has other capabilities.&lt;/p&gt;

&lt;p&gt;Using a password to join the domain is simple:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ adcli join -U username@EXAMPLE.COM -D EXAMPLE.COM --service-name=nfs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Note: the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--service-name=nfs&lt;/code&gt; was used in case you are using an nfs service.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Enter your password. Then if all goes well, you will have joined the domain. Test if you got the right tickets:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ klist -ke
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If, however, you prefer to cache your credentials on the image so that it can be reused by the clones for joining the domain, follow the instructions below. Using cache means that you need to rebuild the image so that clones can be created before these cached credentials get expired.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ kinit username@EXAMPLE.COM -f -A -r 30d -l 30d -c /root/mycachedcredentials
$ adcli join --login-ccache=/root/mycachedcredentials -D EXAMPLE.COM --service-name=nfs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There, no need of typing a password, as your credentials were read from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mycachedcredentials&lt;/code&gt; file. You can save it whenever you want.&lt;/p&gt;

&lt;p&gt;If all goes well and you joined the domain, then let’s make the final configurations.&lt;/p&gt;

&lt;h4 id=&quot;configuration-for-the-horizon-view-agent&quot;&gt;Configuration for the Horizon View Agent&lt;/h4&gt;

&lt;p&gt;Ok, as I said before, the clones need to rejoin the domain, right? This is because they will have their own hostname, other ip addresses, etc., so it needs to join AD again. We need to orchestrate this. Luckly, VMWare helps us here with some settings.&lt;/p&gt;

&lt;p&gt;What you do want to do now is to create a script that will configure your clones with some settings and join the domain.&lt;/p&gt;

&lt;p&gt;Here’s a sample script that does the trick:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;#! /bin/bash
# (Re)joins machine to AD after hostname is changed when instant clones are created.
# Must be called by /etc/vmware/viewagent-custom.conf

LOG=/tmp/ad-join.log # Because logging never killed anyone...
fqdn=&quot;$(hostname).example.com&quot;
hostname=&quot;$(hostname)&quot;
echo &quot;This instant clone will joining ad...&quot; &amp;gt;&amp;gt; $LOG
echo &quot;Host: $(hostname)&quot; &amp;gt;&amp;gt; $LOG

# Set new hostnames
hostname &quot;$fqdn&quot;
cat &amp;lt;&amp;lt;EOF &amp;gt; /etc/hosts
127.0.0.1 $hostname
::1   $hostname
EOF
echo &quot;$fqdn&quot; &amp;gt; /etc/host

# Stop SSSD so that the previous keytab is released from the cache
service sssd stop
sss_cache -E
rm -rf /var/lib/sss/mc/*
rm -rf /var/lib/sss/db/*
rm -rf /var/lib/sss/pubconf/*

# (Re)join AD
rm -rf /etc/krb5.keytab
adcli join --login-ccache=/root/mycachedcredentials -D EXAMPLE.COM --service-name=nfs
test $? -eq 0 || echo &quot;Joining failed&quot; &amp;gt;&amp;gt; $LOG
sleep 5
rm -rf /root/mycachedcredentials # Why leave your credentials on that clone? 

# Restart SSSD so we can authenticate again
service sssd start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Save this as, for example, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local/bin/ad-join.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to use a password instead of the cached credentials, change tthe adcli line above to:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;echo &quot;myfancypassword&quot; | adcli join --stdin-password -join -U username@EXAMPLE.COM -D EXAMPLE.COM --service-name=nfs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There is one detail that you might have seen here: we’re stopping sssd and removing all its cache. This is important because the older keytab is still referred to by sssd. You get plenty of errors like this one if you don’t stop sssd and refresh the cache prior rejoining the domain:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Failed to initialize credentials using keytab [MEMORY:/etc/krb5.keytab]: Preauthentication failed. Unable to create GSSAPI-encrypted LDAP connection.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This error was driving me crazy, and if you google it, most of the information will tell you that you need to remove the /etc/krb5.keytab, delete the account on AD, etc., but actually most of these things won’t be necessary (Note that I do remove the old keytab). Stopping sssd and removing its cache prior rejoining the domain did the trick to fix this. Credits to &lt;a href=&quot;https://access.redhat.com/discussions/2880321&quot;&gt;this forum post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, one last step. Open &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/vmware/viewagent-custom.conf&lt;/code&gt; and edit the following lines:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;...
RunOnceScript=/usr/local/bin/ad-join.sh # or whatever the path/name you chose for your script
RunOnceScriptTimeout=120 # Adjust here if you believe that your rejoins take more than 2 minutes.
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it! Now your image is fully configured for joining the domain. Shut it down, and create a snapshot with your modifications on vCenter, and schedule it to boot from your Horizon connection server.&lt;/p&gt;

&lt;p&gt;But if you want to mount NFS shares as home directories, do the following step before shutting down your machine:&lt;/p&gt;

&lt;h4 id=&quot;setting-up-pam_mount-for-mounting-home-directories&quot;&gt;Setting up &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pam_mount&lt;/code&gt; for mounting home directories&lt;/h4&gt;

&lt;p&gt;There are a few ways to automount home directories, such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;autofs&lt;/code&gt;. I used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pam_mount&lt;/code&gt;, but might change it in the future for the flexibility of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;autofs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Using NFSv4 with kerberos is a very good solution to mount those directories. If you got a kerberized NFSv4 server, why not?&lt;/p&gt;

&lt;p&gt;With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pam_mount&lt;/code&gt;, this was my configuration at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/security/pam_mount.conf.xml&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; &amp;lt;volume
 fstype=&quot;nfs&quot;
 server=&quot;serveraddress&quot;
 uid=&quot;10000-100000&quot; 
 path=&quot;/homedir/%(USER)&quot;
 mountpoint=&quot;/home/%(USER)&quot;
 options=&quot;vers=4,sec=krb5&quot;
 noroot=&quot;0&quot;
 /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id=&quot;10000-10000&quot;&lt;/code&gt; - remove it or adjust it accordingly. It basically means that we restrict mounting attempt to those uid’s.&lt;/p&gt;

&lt;p&gt;Now a last configuration: . On the file &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/idmapd.conf&lt;/code&gt;, enter your domain:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Domain = example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Lastly, add the following line to the end of your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/pam.d/password-auth&lt;/code&gt; file in order to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pam_mmount&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;session     optional                                     pam_mount.so disable_pam_password disable_interactive
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Things that might go wrong with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pam_mount&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;SELinux is a $@#!$. Check your logs. If you use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;logout&lt;/code&gt; configuration of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pam_mount&lt;/code&gt;, it might require some adjustments of SELinux - you will which ones on your logs. I had to add two exceptions for SELinux when using pam_mount, one of them related to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ofl&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Check your DNS. Do you get reverse domain lookups working, for example?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;final-remarks&quot;&gt;Final remarks&lt;/h3&gt;

&lt;p&gt;I hope this can help supplementing the information found on VMWare docs on how to integrate Linux to Active Directory with the purpose of offering pools of instant clones.&lt;/p&gt;

&lt;p&gt;Any remarks, comments or tips? Feel free to comment!&lt;/p&gt;

</description>
        <pubDate>Tue, 24 Nov 2020 17:20:23 +0100</pubDate>
        <link>https://francisaugusto.com/2020/Setuo-Linux-Image-for-Horizon-Instant-Clones-with-Active-Directory/</link>
        <guid isPermaLink="true">https://francisaugusto.com/2020/Setuo-Linux-Image-for-Horizon-Instant-Clones-with-Active-Directory/</guid>
        
        <category>vmware</category>
        
        <category>horizon</category>
        
        
        <category>personal</category>
        
      </item>
    
  </channel>
</rss>
