Bridgetown2023-07-23T15:47:57+02:00https://onprem.wtf/feed.xmlonprem.wtfonprem.wtf is the evolution of ntsystems.it. It contains a complete archive of the old site as well as new content.Configure Sign in with Apple for Azure Static Web App2023-07-22T00:00:00+02:002023-07-22T00:00:00+02:00repo://posts.collection/_posts/2023-07-22-setup-sign-in-with-apple-with-azure-swa.md<p>I’m working on a side project and decided to use <a href="https://developer.apple.com/documentation/sign_in_with_apple">Sign in with Apple</a> as an additional authentication provider, next to AAD that I would normally use.</p>
<p>As per Microsoft’s <a href="https://learn.microsoft.com/en-us/azure/static-web-apps/authentication-custom?tabs=apple%2Cinvitations">documentation</a>, adding apple as custom authentication provider is simple enough, just add the following lines to your <code class="highlighter-rouge">staticwebapp.config.json</code> file:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"identityProviders"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"apple"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"registration"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"clientIdSettingName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"APPLE_CLIENT_ID"</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientSecretSettingName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"APPLE_CLIENT_SECRET"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Then we add the two setting names to the Web App’s configuration and we’re done, right? Almost.</p>
<p>First we have to take a deep-dive into JWT as the <code class="highlighter-rouge">APPLE_CLIENT_SECRET</code> must be a signed JWT. There is no documentation specifically for Static Web Apps but this one for <a href="https://learn.microsoft.com/en-us/azure/app-service/configure-authentication-provider-apple">App Service</a> is close enough.</p>
<p>I converted the sample C# code to powershell for easier use. The required package have a dependency loop and so, instead of easily installing them with <code class="highlighter-rouge">Install-Package</code> I had to use <code class="highlighter-rouge">nuget</code>. Follow the documentation above to obtain client id, team id, key id, and the key in pkcs8 format, then use the following snippet to generate the client secret:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Get-AppleClientSecret</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$TeamId</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientId</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$KeyId</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">System.IO.FileInfo</span><span class="p">]</span><span class="nv">$P8KeyFile</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="nv">$p8Content</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Content</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$p8keyfile</span><span class="w">
</span><span class="nv">$p8key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$p8Content</span><span class="o">.</span><span class="nf">where</span><span class="p">{</span><span class="bp">$_</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s2">"^---"</span><span class="p">}</span><span class="w"> </span><span class="o">-join</span><span class="p">(</span><span class="s2">""</span><span class="p">)</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$audience</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://appleid.apple.com"</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$issuer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$teamId</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$clientId</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$kid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$keyId</span><span class="w">
</span><span class="nv">$Claims</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nt">-TypeName</span><span class="w"> </span><span class="nx">System.Collections.Generic.List</span><span class="p">[</span><span class="n">System.Security.Claims.Claim</span><span class="p">]</span><span class="w">
</span><span class="nv">$Claims</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="w">
</span><span class="p">(</span><span class="n">New-Object</span><span class="w"> </span><span class="nt">-TypeName</span><span class="w"> </span><span class="nx">System.Security.Claims.Claim</span><span class="p">(</span><span class="s2">"sub"</span><span class="p">,</span><span class="w"> </span><span class="nv">$subject</span><span class="p">))</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="nv">$cngKey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">System.Security.Cryptography.CngKey</span><span class="p">]::</span><span class="n">Import</span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">Convert</span><span class="p">]::</span><span class="n">FromBase64String</span><span class="p">(</span><span class="nv">$p8key</span><span class="p">),</span><span class="w">
</span><span class="p">[</span><span class="n">System.Security.Cryptography.CngKeyBlobFormat</span><span class="p">]::</span><span class="n">Pkcs8PrivateBlob</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="nv">$signingCred</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nx">Microsoft.IdentityModel.Tokens.SigningCredentials</span><span class="p">(</span><span class="w">
</span><span class="p">(</span><span class="n">New-Object</span><span class="w"> </span><span class="nx">Microsoft.IdentityModel.Tokens.ECDsaSecurityKey</span><span class="p">(</span><span class="w">
</span><span class="p">(</span><span class="n">New-Object</span><span class="w"> </span><span class="nx">System.Security.Cryptography.ECDsaCng</span><span class="p">(</span><span class="nv">$cngKey</span><span class="p">)</span><span class="w">
</span><span class="p">))),</span><span class="w">
</span><span class="s2">"ES256"</span><span class="w"> </span><span class="c"># EcdsaSha256</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="nv">$NotBefore</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Date</span><span class="w">
</span><span class="nv">$Expires</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="nx">180</span><span class="p">)</span><span class="w">
</span><span class="nv">$token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nt">-TypeName</span><span class="w"> </span><span class="nx">System.IdentityModel.Tokens.Jwt.JwtSecurityToken</span><span class="p">(</span><span class="w">
</span><span class="nv">$Issuer</span><span class="p">,</span><span class="w">
</span><span class="nv">$Audience</span><span class="p">,</span><span class="w">
</span><span class="nv">$Claims</span><span class="p">,</span><span class="w">
</span><span class="nv">$NotBefore</span><span class="p">,</span><span class="w">
</span><span class="nv">$Expires</span><span class="p">,</span><span class="w">
</span><span class="nv">$signingCred</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="bp">$null</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$token</span><span class="o">.</span><span class="nf">Header</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"kid"</span><span class="p">,</span><span class="w"> </span><span class="nv">$kid</span><span class="p">)</span><span class="w">
</span><span class="bp">$null</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$token</span><span class="o">.</span><span class="nf">Header</span><span class="o">.</span><span class="nf">Remove</span><span class="p">(</span><span class="s2">"typ"</span><span class="p">)</span><span class="w">
</span><span class="nv">$tokenHandler</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nx">System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler</span><span class="w">
</span><span class="kr">return</span><span class="w"> </span><span class="nv">$tokenHandler</span><span class="o">.</span><span class="nf">WriteToken</span><span class="p">(</span><span class="nv">$token</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># install & import requirements</span><span class="w">
</span><span class="n">Invoke-WebRequest</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="s1">'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe'</span><span class="w"> </span><span class="nt">-OutFile</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">USERPROFILE</span><span class="s2">\Downloads\nuget.exe"</span><span class="w">
</span><span class="o">&</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">USERPROFILE</span><span class="s2">\Downloads\nuget.exe"</span><span class="w"> </span><span class="n">Install</span><span class="w"> </span><span class="nx">System.IdentityModel.Tokens.Jwt</span><span class="w"> </span><span class="nt">-Version</span><span class="w"> </span><span class="nx">6.32.0</span><span class="w"> </span><span class="nt">-OutputDirectory</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">USERPROFILE</span><span class="s2">\Downloads\.nuget"</span><span class="w">
</span><span class="n">Add-Type</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">USERPROFILE</span><span class="s2">\.nuget\packages\Microsoft.IdentityModel.Tokens\6.32.0\lib\netstandard2.0\Microsoft.IdentityModel.Tokens.dll"</span><span class="w">
</span><span class="n">Add-Type</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">USERPROFILE</span><span class="s2">\.nuget\packages\System.IdentityModel.Tokens.Jwt\6.32.0\lib\netstandard2.0\System.IdentityModel.Tokens.Jwt.dll"</span><span class="w">
</span><span class="n">Add-Type</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">USERPROFILE</span><span class="s2">\.nuget\packages\Microsoft.IdentityModel.Logging\6.32.0\lib\netstandard2.0\Microsoft.IdentityModel.Logging.dll"</span><span class="w">
</span><span class="n">Add-Type</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">USERPROFILE</span><span class="s2">\.nuget\packages\Microsoft.IdentityModel.JsonWebTokens\6.32.0\lib\netstandard2.0\Microsoft.IdentityModel.JsonWebTokens.dll"</span><span class="w">
</span><span class="n">Add-Type</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">USERPROFILE</span><span class="s2">\.nuget\packages\microsoft.identitymodel.abstractions\6.32.0\lib\netstandard2.0\Microsoft.IdentityModel.Abstractions.dll"</span><span class="w">
</span><span class="c"># set parameters and get secret</span><span class="w">
</span><span class="nv">$p</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">TeamId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Q847A7FG64"</span><span class="w">
</span><span class="nx">ClientId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"wtf.onprem.appleid"</span><span class="w">
</span><span class="nx">KeyId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"8QKD4J6XDZ"</span><span class="w">
</span><span class="nx">P8KeyFile</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">".\AuthKey_8QKD4J6XDZ.p8"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="n">Get-AppleClientSecret</span><span class="w"> </span><span class="err">@</span><span class="nx">p</span><span class="w">
</span></code></pre></div></div>
<p>Once we have obtained the <code class="highlighter-rouge">APPLE_CLIENT_SECRET</code>, the sign in process with Apple should be successful. However, after signing in, we are redirected to the Static Web App and greeted with a <code class="highlighter-rouge">403: Forbidden</code>:</p>
<blockquote>
<p>We need an email address or a handle from your login service. To use this login, please update your account with the missing info.</p>
</blockquote>
<p>It turns out the <code class="highlighter-rouge">identityProviders</code> example given in Microsoft’s documentation is not quite complete. Sign in with Apple only inserts the user’s email address as claim in the authentication token if we specifically ask for it during the sing in process. To do that, we have to add the <code class="highlighter-rouge">scopes</code> property to the <code class="highlighter-rouge">staticwebapp.config.json</code> file:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"identityProviders"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"apple"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"registration"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"clientIdSettingName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"APPLE_CLIENT_ID"</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientSecretSettingName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"APPLE_CLIENT_SECRET"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"login"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"scopes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"email"</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Adding the scope will allow new users to sign in successfully. Users that had already signed in must first remove the app from Sign in with Apple in their <a href="https://appleid.apple.com/account/manage">Apple account</a>.</p>
<p>So, after adding the scope and removing the app from Sign in with Apple, we can finally attempt to sign in again and we should see that the user has to consent sharing their email address in the sign in process:</p>
<figure>
<a href="/assets/2023/2023-07-22_23-11-08.png">
<img src="/assets/2023/2023-07-22_23-11-08.png" alt="" />
</a>
</figure>Tom TorgglerHow to configure Sign in with Apple for Azure Static Web App.Mastodon and WebFinger2022-12-20T00:00:00+01:002022-12-20T00:00:00+01:00repo://posts.collection/_posts/2022-12-20-mastodon-and-webfinger.md<p>With the uncertainty surrounding Twitter I have decided to set up an account on Mastodon. If you haven’t heard about it <a href="https://joinmastodon.org/">Mastodon</a> is an de-centralized, open-source alternative to centralized social media. It is powered by open protocols such as <a href="https://www.w3.org/TR/activitypub/">ActivityPub</a> and <a href="https://www.rfc-editor.org/rfc/rfc7033">WebFinger</a> which allow federation of individual servers (called instances).</p>
<p>If a user on one server searches for a user on another server, they will enter the full name, i.e. @user@example.com, into the search field. The server will then look for information about the user at the path <code class="highlighter-rouge">https://example.com/.well-known/webfinger</code>. If found, the reply contains information about where the profile of the user can be found.</p>
<p>We can use this protocol to be discoverable by servers on our own domain. We are using <a href="https://www.bridgetownrb.com/">Bridgetown</a> to build this site so placing a JSON file at this path <code class="highlighter-rouge">src/.well-known/webfinger</code> did the trick. So even though my profile is currently hosted at <code class="highlighter-rouge">masto.ai</code> you can still find me with <code class="highlighter-rouge">@tom@onprem.wtf</code>. And if you do find me, give me a follow :)</p>
<p>I used this PowerShell function to test the WebFinger endpoint on our and other sites.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-WebFinger</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">ValueFromPipeline</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">ValidatePattern</span><span class="p">(</span><span class="s1">'^@?[\d\w]+@[\d\w]+\.[\d\w]+'</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Uri</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Server</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Username</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="p">(</span><span class="nv">$Uri</span><span class="p">){</span><span class="w">
</span><span class="nv">$Username</span><span class="p">,</span><span class="w"> </span><span class="nv">$server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$uri</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="s1">'^@'</span><span class="w"> </span><span class="o">-split</span><span class="w"> </span><span class="s1">'@'</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$webFingerUri</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://</span><span class="nv">$server</span><span class="s2">/.well-known/webfinger?resource=acct:</span><span class="nv">$Username</span><span class="s2">@</span><span class="nv">$Server</span><span class="s2">"</span><span class="w">
</span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"GET </span><span class="nv">$webFingerUri</span><span class="s2">"</span><span class="w">
</span><span class="nv">$r</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-RestMethod</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="nv">$webFingerUri</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="nx">Uri</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Username</span><span class="p">,</span><span class="nv">$Server</span><span class="w"> </span><span class="err">-</span><span class="nx">join</span><span class="w"> </span><span class="s1">'@'</span><span class="w">
</span><span class="nx">Subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$r</span><span class="err">.</span><span class="nx">subject</span><span class="w">
</span><span class="nx">Aliases</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$r</span><span class="err">.</span><span class="nx">Aliases</span><span class="w">
</span><span class="nx">ProfilePage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$r</span><span class="err">.</span><span class="nx">links</span><span class="err">.</span><span class="nx">where</span><span class="p">{</span><span class="bp">$_</span><span class="o">.</span><span class="nf">rel</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'http://webfinger.net/rel/profile-page'</span><span class="p">}</span><span class="err">.</span><span class="nx">href</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Examples</span><span class="w">
</span><span class="s1">'tto@masto.ai'</span><span class="p">,</span><span class="s1">'@tom@onprem.wtf'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Invoke-WebFinger</span><span class="w">
</span><span class="n">Invoke-WebFinger</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="nx">tom</span><span class="err">@</span><span class="nx">onprem.wtf</span><span class="w">
</span><span class="n">Invoke-WebFinger</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="nx">onprem.wtf</span><span class="w"> </span><span class="nt">-Username</span><span class="w"> </span><span class="nx">tom</span><span class="w">
</span></code></pre></div></div>
<p>Other people wrote about this:</p>
<ul>
<li><a href="https://www.hanselman.com/blog/use-your-own-user-domain-for-mastodon-discoverability-with-the-webfinger-protocol-without-hosting-a-server">Use your own user @ domain for Mastodon discoverability with the WebFinger Protocol without hosting a server</a></li>
<li><a href="https://blog.maartenballiauw.be/post/2022/11/05/mastodon-own-donain-without-hosting-server.html">Mastodon on your own domain without hosting a server</a></li>
<li><a href="https://rpm.sh/custom-mastodon-domain/">Using Cloudflare to Customize Your Mastodon Username Domain</a></li>
</ul>Tom TorgglerA simple JSON file tells servers in the fediverse where to find me.How to connect to Exchange Online powershell with a managed identity2022-08-31T00:00:00+02:002022-08-31T00:00:00+02:00repo://posts.collection/_posts/2022-08-31-how-to-connect-exchange-online-managed-identity.md<p>The latest preview version of the ExchangeOnlineManagement powershell module includes the following new parameters: <code class="highlighter-rouge">-ManagedIdentity</code> and <code class="highlighter-rouge">-ManagedIdentityAccountId</code>.</p>
<p>As their names imply, they can be used to connect to Exchange Online with a managed identity. According to the documentation this is currently only supported with Azure Virtual Machines and Virtual Machine Scale Sets, however I have used this successfully within Azure Automation runbooks.</p>
<h2 id="create-the-automation-account">Create the automation account</h2>
<p>If you have an existing account skip this step. I will be reusing the variables from this first example, so fill in the name of your automation account and the resource group.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$accountName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'azautomation1'</span><span class="w">
</span><span class="nv">$rgName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'onprem-core'</span><span class="w">
</span><span class="nv">$location</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'West Europe'</span><span class="w">
</span><span class="n">Connect-AzAccount</span><span class="w">
</span><span class="nx">New-AzAutomationAccount</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nv">$accountName</span><span class="w"> </span><span class="nt">-ResourceGroupName</span><span class="w"> </span><span class="nv">$rgName</span><span class="w"> </span><span class="nt">-Location</span><span class="w"> </span><span class="nv">$location</span><span class="w">
</span></code></pre></div></div>
<h2 id="get-the-module">Get the module</h2>
<p>The first step is to add the module to the Automation Account. Installing it through the Azure Portal did not work, as that way only seems to support the latest non-preview version. I used the following commands from the Az powershell module to install the preview version of the module in my automation account:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$moduleName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'ExchangeOnlineManagement'</span><span class="w">
</span><span class="nv">$moduleVersion</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'2.0.6-Preview7'</span><span class="w">
</span><span class="n">New-AzAutomationModule</span><span class="w"> </span><span class="nt">-AutomationAccountName</span><span class="w"> </span><span class="nv">$accountName</span><span class="w"> </span><span class="nt">-ResourceGroupName</span><span class="w"> </span><span class="nv">$rgName</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nv">$moduleName</span><span class="w"> </span><span class="nt">-ContentLinkUri</span><span class="w"> </span><span class="s2">"https://www.powershellgallery.com/api/v2/package/</span><span class="nv">$moduleName</span><span class="s2">/</span><span class="nv">$moduleVersion</span><span class="s2">"</span><span class="w">
</span></code></pre></div></div>
<h2 id="managed-identity">Managed Identity</h2>
<p>Now it’s time to enable the system assigned managed identity for the automation account. We can do this through the Azure portal by navigating to the automation account and setting the <em>Status</em> to <em>On</em> under <em>Identity</em>. Alternatively, we can use the Az powershell module like this:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Set-AzAutomationAccount</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nv">$accountName</span><span class="w"> </span><span class="nt">-ResourceGroupName</span><span class="w"> </span><span class="nv">$rgName</span><span class="w"> </span><span class="nt">-AssignSystemIdentity</span><span class="w">
</span></code></pre></div></div>
<p>Next we will need the id of the managed identity. It will show up in the Azure portal once it has been enabled or it can be retrieved with Az powershell:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-AzADServicePrincipal</span><span class="w"> </span><span class="nt">-DisplayName</span><span class="w"> </span><span class="nv">$accountName</span><span class="w">
</span></code></pre></div></div>
<p>In my case the object id is <code class="highlighter-rouge">b395da15-4904-490c-9109-2bc91a12a08d</code>. With this id in hand, we use the Microsoft Graph powershell SDK to grant the necessary permissions to the managed identity.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Connect-MgGraph</span><span class="w">
</span><span class="nv">$params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">ServicePrincipalId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'b395da15-4904-490c-9109-2bc91a12a08d'</span><span class="w"> </span><span class="c"># managed identity object id</span><span class="w">
</span><span class="nx">PrincipalId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'b395da15-4904-490c-9109-2bc91a12a08d'</span><span class="w"> </span><span class="c"># managed identity object id</span><span class="w">
</span><span class="nx">ResourceId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">(</span><span class="nx">Get</span><span class="err">-</span><span class="nx">MgServicePrincipal</span><span class="w"> </span><span class="err">-</span><span class="nx">Filter</span><span class="w"> </span><span class="s2">"AppId eq '00000002-0000-0ff1-ce00-000000000000'"</span><span class="err">).</span><span class="nx">id</span><span class="w"> </span><span class="c"># Exchange online</span><span class="w">
</span><span class="nx">AppRoleId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"dc50a0fb-09a3-484d-be87-e023b12c6440"</span><span class="w"> </span><span class="c"># Exchange.ManageAsApp</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="n">New-MgServicePrincipalAppRoleAssignedTo</span><span class="w"> </span><span class="err">@</span><span class="nx">params</span><span class="w">
</span></code></pre></div></div>
<p>Lastly we want to assign the role <em>Exchange Administrator</em> to the managed identity. Again, we can do this through the Azure portal or with the following command:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$roleId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-MgRoleManagementDirectoryRoleDefinition</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="s2">"DisplayName eq 'Exchange Administrator'"</span><span class="p">)</span><span class="o">.</span><span class="nf">id</span><span class="w">
</span><span class="n">New-MgRoleManagementDirectoryRoleAssignment</span><span class="w"> </span><span class="nt">-PrincipalId</span><span class="w"> </span><span class="nx">b395da15-4904-490c-9109-2bc91a12a08d</span><span class="w"> </span><span class="nt">-RoleDefinitionId</span><span class="w"> </span><span class="nx">29232cdf-9323-42fd-ade2-1d097af3e4de</span><span class="w"> </span><span class="nt">-DirectoryScopeId</span><span class="w"> </span><span class="s2">"/"</span><span class="w">
</span></code></pre></div></div>
<p>Please assign the role with the least amount of privileges to complete the task you need.</p>
<h2 id="connect-to-exchange-online-in-the-runbook">Connect to Exchange Online in the runbook</h2>
<p>After completing the steps above we are ready to connect to Exchange Online using the managed identity in the runbook. If you create a new runbook, please make sure to use runtime version <code class="highlighter-rouge">5.1</code> as the that’s where we have imported the module earlier.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Connect-ExchangeOnline</span><span class="w"> </span><span class="nt">-ManagedIdentity</span><span class="w"> </span><span class="nt">-Organization</span><span class="w"> </span><span class="s1">'onpremwtf.onmicrosoft.com'</span><span class="w">
</span><span class="n">Get-AcceptedDomain</span><span class="w">
</span></code></pre></div></div>
<p>Tom</p>Tom TorgglerConnect to Exchange Online powershell without app registrationGoodbye Jekyll, hello Bridgetown!2022-08-13T00:00:00+02:002022-08-13T00:00:00+02:00repo://posts.collection/_posts/2022-08-13-goodbye-jekyll.md<p>We have been using Jekyll for our little site since 2016. It was fast, simple, it did it’s job nicely. <a href="https://www.bridgetownrb.com/">Bridgetown</a> does all the same things, and much more. It’s Jekyll’s modern cousin.</p>
<p>We have used GitHub pages to host our site as it integrates nicely with Jekyll. I have long wanted to play with Cloudflare pages, so I decided to upgrade the site and move it over to Cloudflare in the progress.</p>
<h1 id="how-to-run-bridgetown-on-cloudflare-pages">How to run Bridgetown on Cloudflare pages?</h1>
<p>Well, it’s easy enough, we just have to things to consider:</p>
<ol>
<li>Include a <code class="highlighter-rouge">.node-version</code> file because Cloudflare pages defaults to <code class="highlighter-rouge">12.18.0</code> and bridgetown requires a version newer than <code class="highlighter-rouge">14</code></li>
<li>Set the <code class="highlighter-rouge">BRIDGETOWN_ENV</code> environment variable to <code class="highlighter-rouge">production</code></li>
</ol>
<p>To tell Cloudflare pages to use a newer version of node, I created the file <code class="highlighter-rouge">.node-version</code> with the content <code class="highlighter-rouge">16.16.0</code> in the root directory of my repository. Just like with Jekyll, the base is a GitHub repository. All that’s left to do is sign in to <a href="https://dash.cloudflare.com">Cloudflare</a> and create a new pages project. I sign in to my GitHub account from Cloudflare, select the repository and enter the following information:</p>
<p>Build command: <code class="highlighter-rouge">bin/bridgetown deploy</code>
Build output directory: <code class="highlighter-rouge">output</code>
Environment variable: <code class="highlighter-rouge">BRIDGETOWN_ENV</code> <code class="highlighter-rouge">production</code></p>
<p>Done.</p>
<h1 id="cloudflare-redirects">Cloudflare redirects</h1>
<p>We have used Jekyll’s <code class="highlighter-rouge">jekyll-redirect-from</code> plugin to create redirects for some URLs. It seems bridgetown does not yet have a <a href="https://www.bridgetownrb.com/plugins">plugin</a> for that, so I used Cloudflare page’s <code class="highlighter-rouge">_redirect</code> file instead. I created a file with the name <code class="highlighter-rouge">_redirect</code> in the <code class="highlighter-rouge">src</code> folder of my bridgetown project. The content of the file is like this:</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/a/very/long/url/path /shorturl 301
</code></pre></div></div>
<p>You can read more in the <a href="https://developers.cloudflare.com/pages/platform/redirects/">docs</a></p>
<p>I used the following few lines of powershell code to find Jekyll’s <code class="highlighter-rouge">redirect_from</code> statements in my source folder, convert them into slugs, and add them to the <code class="highlighter-rouge">_redirect</code> file.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-ChildItem</span><span class="w"> </span><span class="o">.</span><span class="nx">/src/</span><span class="w"> </span><span class="nt">-Recurse</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*.</span><span class="nf">md</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-String</span><span class="w"> </span><span class="nt">-pattern</span><span class="w"> </span><span class="s2">"redirect_from"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$p</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">path</span><span class="w">
</span><span class="nv">$n</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">filename</span><span class="w">
</span><span class="nv">$l</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">line</span><span class="w">
</span><span class="c"># remove date and extension from filename as slug contains neither</span><span class="w">
</span><span class="kr">if</span><span class="p">(</span><span class="nv">$n</span><span class="w"> </span><span class="o">-match</span><span class="w"> </span><span class="s2">"^\d{4}-"</span><span class="p">){</span><span class="w">
</span><span class="nv">$n</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$n</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="s2">"\d{4}-\d{2}-\d{2}-"</span><span class="p">,</span><span class="s2">""</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$n</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$n</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="s2">".md"</span><span class="p">,</span><span class="s2">""</span><span class="w">
</span><span class="nv">$name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$n</span><span class="o">.</span><span class="nf">ToLower</span><span class="p">()</span><span class="w">
</span><span class="c"># find parent path and create slug</span><span class="w">
</span><span class="nv">$p</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Split-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$p</span><span class="w"> </span><span class="nt">-Parent</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Split-Path</span><span class="w"> </span><span class="nt">-Leaf</span><span class="w">
</span><span class="kr">switch</span><span class="p">(</span><span class="nv">$p</span><span class="p">){</span><span class="w">
</span><span class="s2">"_OnlineHelp"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$slug</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"/powershell/</span><span class="nv">$name</span><span class="s2">"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="s2">"Archive"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$slug</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"/post/</span><span class="nv">$name</span><span class="s2">"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="s2">"_Modules"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$slug</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"/powershell/</span><span class="nv">$name</span><span class="s2">"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="s2">"_Scripts"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$slug</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"/powershell/</span><span class="nv">$name</span><span class="s2">"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="s2">"_posts"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$slug</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"/post/</span><span class="nv">$name</span><span class="s2">"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># write _redirects file</span><span class="w">
</span><span class="nv">$l</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="s2">"redirect_from: "</span><span class="p">,</span><span class="s2">""</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"</span><span class="bp">$_</span><span class="s2"> </span><span class="nv">$slug</span><span class="s2"> 301"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Set-Content</span><span class="w"> </span><span class="o">.</span><span class="nx">/src/_redirects</span><span class="w">
</span></code></pre></div></div>Tom TorgglerHow to configure sender authentication for Exchange Online in 20212021-01-30T00:00:00+01:002021-01-30T00:00:00+01:00repo://posts.collection/_posts/2021-01-30-sender-authentication-exo-2021.md<h2 id="the-current-state-of-email">The current state of email</h2>
<p>The year is 2021 and, despite many efforts to kill it off, email is still going strong. According to Microsoft’s latest digital defense <a href="https://www.microsoft.com/en-us/security/business/security-intelligence-report">report</a> Exchange Online Protection processed 6 trillion messages last year, 13 billion of which malicious. 6 trillion, that’s a number with 12 zeros. And that’s just Exchange Online. Worldwide we are sending and receiving over 300 billion emails every day, according to <a href="https://www.statista.com/statistics/456500/daily-number-of-e-mails-worldwide/">this</a> site. 300 billion. Every day.</p>
<p>With these numbers there’s no wonder email is one of the main threat vectors.</p>
<p>As many organizations are moving their mailboxes to Exchange Online, I thought I would share my basic setup for new tenants. This will help you getting started with a secure configuration in no time. I have two goals with this basic setup:</p>
<ol>
<li>Protect your brand (domain) from being spoofed/abused</li>
<li>Protect your users from receiving malicious emails</li>
</ol>
<h2 id="sender-authentication">Sender Authentication</h2>
<p>So you have just signed up for a new tenant with Microsoft 365 and you are adding your custom domains. The wizard will ask you, whether or not you are planning to use Exchange Online and, if you select yes, it will help you setup SPF.</p>
<h3 id="sender-policy-framework">Sender policy framework</h3>
<p>Even though it has only been published as proposed <a href="https://tools.ietf.org/html/rfc7208">standard</a> by the IETF in 2014, SPF has been around since the early 2000’s. Despite it’s age it is still something we regularly find missing or misconfigured in customer environments. SPF gives an administrator of an email domain a way to specify which systems (IP addresses) are allowed to send emails using the domain. The admin publishes a TXT record in the DNS, listing all IP addresses that are allowed to send emails. This would typically be your onprem Exchange servers or email gateways.</p>
<p>Receiving systems check the TXT record and see if the system that’s trying to deliver a message is allowed to do so.</p>
<p>If you want to start sending emails from Exchange Online, you should add the following line to your existing SPF record.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>include: include:spf.protection.outlook.com
</code></pre></div></div>
<p>If you don’t have an SPF record in place, create a new TXT record at the root of your domain with this content:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>v=spf include:spf.protection.outlook.com -all
</code></pre></div></div>
<blockquote title="Note">
<p>If you are not using a domain for outbound email, please publish the following SPF record to make it harder for criminals to abuse your domain:</p>
<p>TXT: <code class="highlighter-rouge">v=spf -all</code></p>
</blockquote>
<p>This is how far the wizard goes but we should really always configure the following records as well.</p>
<h3 id="domainkeys-identified-mail">DomainKeys Identified Mail</h3>
<p>DKIM has been <a href="https://tools.ietf.org/html/rfc6376">standardized</a> in 2011 and, in parts thanks to Exchange Online, is being used widely. However, we find SPF is better known and understood by our customers. DKIM leverages digital signatures that let a receiving system cryptographically verify whether an email was sent by an authorized system or not. The signature includes a domain name (<code class="highlighter-rouge">d=</code>) that should match the domain in the mail from address. Like SPF, DKIM uses DNS records in the sender’s email domain. The administrator of the domain publishes a TXT record that contains a public key and then configures the email server to sign all outgoing messages with the corresponding private key.</p>
<p>Receiving systems see the signature and a so called selector in the header of the email. The selector tells the receiving system where to find the public key to verify the signature. As always with certificates, keys have to be rotated periodically which means DKIM DNS records must be updated accordingly. Sounds like a lot of complicated work, right?</p>
<p>With Exchange Online, Microsoft does that work for you. All outgoing emails from Exchange Online are signed with a key that Microsoft manages. The only thing we have to do, is point our DNS to that key and enable the configuration. There is a lengthy <a href="https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/use-dkim-to-validate-outbound-email?view=o365-worldwide">docs</a> article about DKIM and how to build the DNS records you have to publish. I am using a few lines of PowerShell to make that process easier.</p>
<p>Use the following PowerShell snippet to:</p>
<ol>
<li>Create a new DKIM signing configuration for your custom domain</li>
<li>Publish DNS records pointing to Microsoft domains</li>
<li>Enable the DKIM signing configuration</li>
</ol>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Create DKIM signing config for all domains that do not have one</span><span class="w">
</span><span class="nv">$d</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-DkimSigningConfig</span><span class="w">
</span><span class="nv">$domains</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$d</span><span class="o">.</span><span class="nf">domain</span><span class="w">
</span><span class="n">Get-AcceptedDomain</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="p">{</span><span class="err"></span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">DomainName</span><span class="w"> </span><span class="nt">-in</span><span class="w"> </span><span class="nv">$domains</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="err"></span><span class="p">}</span><span class="err"></span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="err"></span><span class="w"> </span><span class="n">New-DkimSigningConfig</span><span class="w"> </span><span class="nt">-KeySize</span><span class="w"> </span><span class="nx">2048</span><span class="w"> </span><span class="nt">-DomainName</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">DomainName</span><span class="w"> </span><span class="nt">-Enabled</span><span class="w"> </span><span class="bp">$false</span><span class="p">}</span><span class="err"></span><span class="w">
</span><span class="p">}</span><span class="err"></span><span class="w">
</span><span class="c"># Create DNS Records</span><span class="w">
</span><span class="n">Get-DkimSigningConfig</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="nx">Name</span><span class="w"> </span><span class="o">-NotMatch</span><span class="w"> </span><span class="nx">onmicrosoft</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nx">Name</span><span class="p">,</span><span class="o">*</span><span class="nx">cname</span><span class="o">*</span><span class="p">,@{</span><span class="w">
</span><span class="nx">n</span><span class="o">=</span><span class="s2">"Selector1"</span><span class="p">;</span><span class="w">
</span><span class="nx">e</span><span class="o">=</span><span class="p">{(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Selector1CNAME</span><span class="w"> </span><span class="o">-split</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="p">),</span><span class="bp">$_</span><span class="o">.</span><span class="nf">name</span><span class="w"> </span><span class="o">-join</span><span class="w"> </span><span class="s2">"._domainkey."</span><span class="p">}},@{</span><span class="w">
</span><span class="nx">n</span><span class="o">=</span><span class="s2">"Selector2"</span><span class="p">;</span><span class="w">
</span><span class="nx">e</span><span class="o">=</span><span class="p">{(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Selector2CNAME</span><span class="w"> </span><span class="o">-split</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="p">),</span><span class="bp">$_</span><span class="o">.</span><span class="nf">name</span><span class="w"> </span><span class="o">-join</span><span class="w"> </span><span class="s2">"._domainkey."</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Once the DNS records are in place, we can go ahead and enable the DKIM configuration:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-DkimSigningConfig</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Set-DkimSigningConfig</span><span class="w"> </span><span class="nt">-Enabled</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span></code></pre></div></div>
<blockquote>
<p>If you are not using a domain for outbound email, you don’t have to worry about DKIM.</p>
</blockquote>
<h3 id="domain-based-message-authentication-reporting-and-conformance">Domain-based Message Authentication, Reporting and Conformance</h3>
<p>DMARC is the new kid on the bloc. Well kind of, the <a href="https://tools.ietf.org/html/rfc7489">RFC</a> is from 2015. It is yet another DNS record that an administrator can use to tell receiving systems what exactly they should do with emails that fail SPF or DKIM. Essentially DMARC builds on SPF and DKIM and uses both to calculate an authentication result that supports scenarios where SPF alone would fail (forwarding). The DMARC policy is also used to define what should happen with unaligned or failing DKIM signatures as DKIM itself doesn’t really specify that.</p>
<p>So, another DNS record you said? Here we go:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Name: _dmarc.example.com
Type: TXT
Value: v=DMARC1; p=none; pct=100;
</code></pre></div></div>
<blockquote title="Note">
<p>While the SPF record must be published at the root of your domain, the DMARC record must be at _dmarc.</p>
</blockquote>
<p>With DMARC it is recommended to implement monitoring, so we will have to look at an additional tool. I have found the <a href="https://go.valimail.com/microsoft.html">DMARC Monitor</a> from ValiMail is a good option to get started, it is also free for Microsoft 365 customers. There are many alternatives, please check with your security team if you already have a tool. Whichever tool you end up using, it will ask you to update your DMARC record to include an URI of a mailbox to send reports to. The <code class="highlighter-rouge">rua</code> and <code class="highlighter-rouge">ruf</code> tags in the TXT record are used for that, in the case of ValiMail the complete record looks like this:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>v=DMARC1; p=none; pct=100; rua=mailto:dmarc_agg@vali.email;
</code></pre></div></div>
<p>This record tells a receiving system to deliver emails independent of the authentication result (p is set to none) and send aggregated reports (rua) to ValiMail.</p>
<p>With this record in place, you are now ready to send emails from Exchange Online. But we’re not completely done with DMARC just yet.</p>
<p>The ultimate goal is to set the DMARC policy to <code class="highlighter-rouge">p=reject</code> thereby telling any receiving system to reject emails that fail authentication. Before we can do that, we must make sure all legitimate emails pass authentication. The monitoring helps us verify exactly that, the example in the following screenshot shows outbound emails from our systems for the last month. As you can see, all of them authenticated successfully:</p>
<figure>
<a href="/assets/2021/2021-01-30_20-45-13-740.png">
<img src="/assets/2021/2021-01-30_20-45-13-740.png" alt="dmarc monitor results" />
</a>
</figure>
<blockquote>
<p>Exchange Online does currently not send DMARC reports, so if you are sending only to Exchange Online recipients, don’t expect much information in your monitoring.</p>
</blockquote>
<p>Remember that I said from <em>our</em> systems above, now let’s change that filter in ValiMail and look at <em>all</em> emails from our domain. As you can see in the screenshot below, over the same period of time, 90 emails failed DMARC authentication:</p>
<figure>
<a href="/assets/2021/2021-01-30_21-32-31-740.png">
<img src="/assets/2021/2021-01-30_21-32-31-740.png" alt="dmarc monitor results" />
</a>
</figure>
<p>In our case, we already have a reject policy in place, so receiving systems should not accept these emails which are spam or worse. So, after setting up DMARC monitoring with a policy of none, observe the situation for some time and, if you are confident your systems are configured correctly, go ahead and update the record:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>v=DMARC1; p=reject; pct=100; rua=mailto:dmarc_agg@vali.email;
</code></pre></div></div>
<blockquote title="Note">
<p>If you are not using a domain for outbound email, please publish the following DMARC record to make it harder for criminals to abuse your domain:</p>
<p>TXT: v=DMARC1; p=reject; pct=100;</p>
</blockquote>
<p>In the next post we will have a look at preset security policies in Exchange Online Protection.</p>
<p>— Tom.</p>Tom TorgglerLearn how to configure your email domains for Exchange OnlineWhy is this website so fast?2021-01-16T00:00:00+01:002021-01-16T00:00:00+01:00repo://posts.collection/_posts/2021-01-16-why-is-this-website-fast.md<p>I have recently updated our website and have learned a few things while doing so.<!-- more --> As mentioned on the home page, this website is still built with Jekyll. The so-called grandfather of static website generators is maybe not perfect, but the following reasons make it a good option for me.</p>
<ol>
<li>It just works: I don’t have much time to tinker with this side-project, there are no complicated, always changing dependencies like with newer, javascript-based tools (oh and no gigantic <code class="highlighter-rouge">node_modules</code> either)</li>
<li>It’s simple: True how variables are handled is not always intuitive but I have learned enough to use it effectively</li>
<li>GitHub pages: The build process is automated, I just push my changes and the rest is taken care of</li>
</ol>
<h2 id="you-promised-fast-whats-all-this">You promised fast, what’s all this?</h2>
<p>I’m getting there. Fast websites are generally associated with more modern site generators like <a href="https://gatsbyjs.com">Gatsby</a>. These typically use a lot of javascript that makes them fast but also more complicated. I wanted to see, if I could get good results with good old Jekyll.</p>
<p>This site is fast because it’s small and very simple. I’m not saying bigger sites don’t need all that <code class="highlighter-rouge">node_modules</code> goodness, I’m saying small sites - like this one - don’t need it.</p>
<p>This site is also fast, because I’ve added a few extra lines to the <code class="highlighter-rouge">head</code> section of the html. At the end of every post you can find a <code class="highlighter-rouge">next</code> navigation button that brings you to the next post. With a little help from Jekyll, I was able to include the relative URL of the next post as <code class="highlighter-rouge">link</code> to the <code class="highlighter-rouge">head</code> section with the keyword <code class="highlighter-rouge">rel=next</code>. This little keyword tells the browser to download the post whenever it has a free moment:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{% if page.next %}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"next"</span> <span class="na">href=</span><span class="s">"{{ page.next.url }}"</span><span class="nt">></span>
{% endif %}
</code></pre></div></div>
<p>The result is a super fast navigation experience because the target has already been downloaded. I’m also preloading fonts with <code class="highlighter-rouge">rel="preload"</code> and the little CSS we use is inlined.</p>
<h2 id="service-workers">Service workers</h2>
<p>Another thing I learned while looking at modern websites is the concept of <a href="https://developers.google.com/web/fundamentals/primers/service-workers/">service workers</a>. This is a little bit of javascript that can be very powerful indeed. Essentially, a service worker is a script that is installed in the browser when a user opens the website. Once installed it can intercept and handle network requests from the browser to the site. It’s a kind of proxy in your browser just for our site.</p>
<p>I’m using a service worker to create a local cache for use with this site. On the first visit, the service downloads a few static files that visitors will definitely need (fonts, main pages). It uses a cache-first strategy, so whenever the browser requests something, the service worker looks in the cache first and returns any results from there. After that it goes out to the site and retrieves the latest version of what the browser was looking for. If there’s no cache-hit, the resource is fetched from the network.</p>
<p>The service worker needs a <a href="manifest.json">manifest</a> and we have to tell the browser where to find it. We add the manifest to the <code class="highlighter-rouge">head</code> section and use a few lines of javascript to trigger the installation of the service worker. This is the pointer to the manifest file:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><link</span> <span class="na">rel=</span><span class="s">"manifest"</span> <span class="na">href=</span><span class="s">"/manifest.json"</span><span class="nt">></span>
</code></pre></div></div>
<p>And this is the code that registers the service worker in your browser after the page is loaded. I have added a condition to skip the registration if the site is accessed through localhost, which is the case when developing locally:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="dl">'</span><span class="s1">serviceWorker</span><span class="dl">'</span> <span class="k">in</span> <span class="nb">navigator</span> <span class="o">&&</span> <span class="o">!</span><span class="p">(</span><span class="sr">/localhost/</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="p">)))</span> <span class="p">{</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">load</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nb">navigator</span><span class="p">.</span><span class="nx">serviceWorker</span><span class="p">.</span><span class="nx">register</span><span class="p">(</span><span class="dl">'</span><span class="s1">//serviceworker.js</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Servie worker registered.</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="faster-build-times-with-jekyll-on-wsl">Faster build times with Jekyll on WSL</h2>
<p>I have <a href="/post/jekyll-on-wsl/">used</a> Jekyll on Windows 10 leveraging the Windows Subsystem for Linux since 2017. Today I realized that actually storing the files within WSL makes the build time much (much) faster. Until today I stored the repository in my Windows user profile, something like <code class="highlighter-rouge">C:\users\tto\...</code>. In the WSL I happily changed into this directory following <code class="highlighter-rouge">/mnt/C/Users...</code> before running <code class="highlighter-rouge">jekyll serve</code>. Build time was around 5 minutes. Not great.</p>
<p>Today it ocurred to me to clone the repository again, this time into the WSL. So I can access it using a WSL path, something like <code class="highlighter-rouge">/home/tto/...</code>. Build time is now less than one minute. Not bad.</p>
<h2 id="webpagetest-results">WebpageTest results</h2>
<p>You don’t have to take my word for it, <a href="https://webpagetest.org/result/210118_DiMH_f1f144faef540fc0069cf3b56982c94c/">webpagetest</a> also thinks this website is pretty fast:</p>
<figure>
<a href="/assets/2021/2021-01-18_9-37-430.gif">
<img src="/assets/2021/2021-01-18_9-37-430.gif" alt="webpagetest results" />
</a>
</figure>
<p>— Tom</p>Tom TorgglerSo long, 20202020-12-29T00:00:00+01:002020-12-29T00:00:00+01:00repo://posts.collection/_posts/2020-12-29-so-long-2020.md<p>Goodbye 2020! What a year it has been.</p>
<p>True, I had to cancel some trips and I stayed home a lot more than usual, but apart from that, it was a pretty spectacular year for me. I moved to Amsterdam from a smaller city in the outskirts. I was able to go to Zürich, Sardinia, Marbella, Napoli and a few other places. In a prolonged episode of lockdown blues I managed to get about 1000 km on my road bike done. However, that was sometime before the summer, since then I have been (a lot) less active. I worked a lot. I learned a lot.</p>
<p>Since a certain global event made more travelling impossible, or at least not very recommendable, I’m staying in Amsterdam for the holiday season and started building this new site.
After more than 10 years, it’s now time to say: Goodbye ntSystems.it</p>
<h2 id="welcome-onpremwtf">Welcome onprem.wtf</h2>
<p>Welcome to our new home. This site is still build with <a href="https://jekyllrb.com">Jekyll</a> and hosted on <a href="https://pages.github.com">GitHub pages</a>. I replaced the default minima theme with a custom one, mostly because I wanted to learn and experiment with some CSS features. I also had a good look at some of the more modern static site generators and other alternatives but decided to stick with Jekyll because it is simple and I really don’t need all of that <code class="highlighter-rouge">node_modules</code> stuff for just a simple blog.</p>
<p>The new site has a simpler layout with less clutter, should be easier to read on any screen, and it also has a dark theme. Actually, unless you select a theme, it will use the same settings that your operating system (or browser) uses. Pretty slick, right?</p>
<p>What about the domain name you ask? Well, this one was available and it kind of fits with what we are doing at the moment. If you find a better one, I’m open to suggestions 😉</p>
<h2 id="whats-next">What’s next?</h2>
<p>Well that’s a good question. For now, like many others, I’ll be watching how the vaccine rollout is going and what’s happening in the US of A. I would like to become a better writer so I will try to publish my thoughts more regularly. We’ll see how that goes.</p>
<p>With that I wish you all the best for the new year.</p>
<p>— Tom.</p>Tom TorgglerConvert PowerShell Help to a Website2020-12-21T00:00:00+01:002020-12-21T00:00:00+01:00repo://posts.collection/_posts/2020-12-21-converting-powershell-help-to-a-website.md<p>How to use the platyPS PowerShell module to convert comment-based help to markdown and easily host it on GitHub pages.</p>
<!-- more -->
<p>Now you might have read that our blog is powered by <a href="/post/welcome-to-the-all-new-ntsystems/">Jekyll</a>, which is a static-site generator that turns markdown files into html. So, obviously, if I would be able to convert PowerShell help content to markdown files, I could simply put them into a folder an serve them via the blog.</p>
<h2 id="create-markdown-files">Create markdown files</h2>
<p>The first step is to install platyPS (available on the PS Gallery) and create the markdown files for every function.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nx">platyPS</span><span class="w">
</span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">platyPS</span><span class="p">,</span><span class="w"> </span><span class="nx">TAK</span><span class="p">,</span><span class="w"> </span><span class="nx">PSSpeech</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$cmdlet</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Command</span><span class="w"> </span><span class="nt">-Module</span><span class="w"> </span><span class="nx">PSSpeech</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$h</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Help</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nv">$cmdlet</span><span class="o">.</span><span class="nf">Name</span><span class="p">)</span><span class="w">
</span><span class="nv">$meta</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="s1">'layout'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'pshelp'</span><span class="p">;</span><span class="w">
</span><span class="s1">'author'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'tto'</span><span class="p">;</span><span class="w">
</span><span class="s1">'title'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">$(</span><span class="nv">$cmdlet</span><span class="err">.</span><span class="nx">Name</span><span class="err">)</span><span class="p">;</span><span class="w">
</span><span class="s1">'category'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">$(</span><span class="nv">$cmdlet</span><span class="err">.</span><span class="nx">ModuleName</span><span class="err">.</span><span class="nx">ToLower</span><span class="err">())</span><span class="p">;</span><span class="w">
</span><span class="s1">'excerpt'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="se">`"</span><span class="si">$(</span><span class="nv">$h</span><span class="o">.</span><span class="nf">Synopsis</span><span class="si">)</span><span class="se">`"</span><span class="s2">"</span><span class="p">;</span><span class="w">
</span><span class="s1">'date'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">$(</span><span class="nx">Get</span><span class="err">-</span><span class="nx">Date</span><span class="w"> </span><span class="err">-</span><span class="nx">Format</span><span class="w"> </span><span class="nx">yyyy</span><span class="err">-</span><span class="nx">MM</span><span class="err">-</span><span class="nx">dd</span><span class="err">)</span><span class="p">;</span><span class="w">
</span><span class="s1">'redirect_from'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"[</span><span class="se">`"</span><span class="s2">/PowerShell/</span><span class="si">$(</span><span class="nv">$cmdlet</span><span class="o">.</span><span class="nf">ModuleName</span><span class="si">)</span><span class="s2">/</span><span class="si">$(</span><span class="nv">$cmdlet</span><span class="o">.</span><span class="nf">Name</span><span class="si">)</span><span class="s2">/</span><span class="se">`"</span><span class="s2">, </span><span class="se">`"</span><span class="s2">/PowerShell/</span><span class="si">$(</span><span class="nv">$cmdlet</span><span class="o">.</span><span class="nf">ModuleName</span><span class="si">)</span><span class="s2">/</span><span class="si">$(</span><span class="nv">$cmdlet</span><span class="o">.</span><span class="nf">Name</span><span class="o">.</span><span class="nf">ToLower</span><span class="p">()</span><span class="si">)</span><span class="s2">/</span><span class="se">`"</span><span class="s2">, </span><span class="se">`"</span><span class="s2">/PowerShell/</span><span class="si">$(</span><span class="nv">$cmdlet</span><span class="o">.</span><span class="nf">Name</span><span class="o">.</span><span class="nf">ToLower</span><span class="p">()</span><span class="si">)</span><span class="s2">/</span><span class="se">`"</span><span class="s2">]"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="p">(</span><span class="nv">$h</span><span class="o">.</span><span class="nf">Synopsis</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s2">"\[|\]"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">New-MarkdownHelp</span><span class="w"> </span><span class="nt">-Command</span><span class="w"> </span><span class="nv">$cmdlet</span><span class="w"> </span><span class="nt">-OutputFolder</span><span class="w"> </span><span class="o">.</span><span class="nx">\_OnlineHelp\a</span><span class="w"> </span><span class="nt">-Metadata</span><span class="w"> </span><span class="nv">$meta</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>The above example creates a <code class="highlighter-rouge">.md</code> help file for every function in the <code class="highlighter-rouge">TAK</code> module. The files are almost ready to be used by our Jekyll-powered blog, I’m using the <code class="highlighter-rouge">-Metadata</code> parameter to add some additional information to the ‘front matter’ of each file.</p>
<blockquote title="Note">
<p>I could be using <code class="highlighter-rouge">New-MarkdownHelp -Module TAK</code> but that way, I was not able to include the metadata automatically.</p>
</blockquote>
<h2 id="rename-files-for-jekyll">Rename files for Jekyll</h2>
<p>The only thing that I have to do now, in order to have Jekyll pick up the files and create websites, is to rename them accordingly.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$file</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="p">(</span><span class="n">Get-ChildItem</span><span class="w"> </span><span class="s1">'.\tak-md-help\*.md'</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$timestamp</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s1">'yyyy-MM-dd'</span><span class="p">)</span><span class="w">
</span><span class="nv">$NewName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$timestamp</span><span class="p">,</span><span class="w"> </span><span class="nv">$file</span><span class="o">.</span><span class="nf">name</span><span class="w"> </span><span class="o">-join</span><span class="w"> </span><span class="s1">'-'</span><span class="w">
</span><span class="n">Rename-Item</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$file</span><span class="o">.</span><span class="nf">FullName</span><span class="w"> </span><span class="nt">-NewName</span><span class="w"> </span><span class="nv">$NewName</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>The above example renames all <code class="highlighter-rouge">*.md</code> files in the <code class="highlighter-rouge">tak-md-help</code> folder to include a timestamp. This step is not necessary if you are using a collection in Jekyll.</p>
<h2 id="include-helpuri">Include HelpUri</h2>
<p>The <code class="highlighter-rouge">Get-Help</code> command has an <code class="highlighter-rouge">-Online</code> parameter, that can be used to easily open a related link when looking for help. To include this functionality in my scripts, I just have to put the URL of the online article in the <code class="highlighter-rouge">[CmdletBinding()]</code> statement, like so:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">(</span><span class="n">HelpUri</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'https://ntsystems.it/PowerShell/TAK/test-tlsconnection/'</span><span class="p">)]</span><span class="w">
</span></code></pre></div></div>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://github.com/PowerShell/platyPS">https://github.com/PowerShell/platyPS</a></li>
</ul>
<p>That’s it :)</p>
<p>Tom</p>Tom TorgglerUsing PowerShell and Azure Cognitive Services to convert text to speech2020-01-11T15:19:00+01:002020-01-11T15:19:00+01:00repo://posts.collection/_posts/2020-01-11-using-powershell-and-azure-cognitive-services-to-convert-text-to-speech.md<p>In one of our recent Microsoft Teams projects I needed some voice prompts for a customer service call queue.<!-- more --> I figured it would be nice to have Azure’s artificial-intelligence-powered speech service convert my text input to an audio file. Turns out it’s easier than I thought it would be.</p>
<h2 id="azure-cognitive-speech-service">Azure Cognitive Speech Service</h2>
<p>First of all we need an Azure Subscription where we can deploy our Speech Services instance. If you don’t have an Azure subscription, you can sign up for a trial account using the links below. If you already have a subscription, you can easily create a free Speech Services account using the following commands from Azure Cloud Shell:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az group create -n ntsystems-speech -l WestEurope
az cognitiveservices account create -n ntsystems-speech -g ntsystems-speech --kind SpeechServices --sku F0 -l WestEurope --yes
</code></pre></div></div>
<p>Now the account was created and we can start using it right away. To authenticate our calls from PowerShell, we need an API key, again we can use Azure Cloud Shell to retrieve the key:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az cognitiveservices account keys list -n ntsystems-speech -g ntsystems-speech
</code></pre></div></div>
<h2 id="powershell">PowerShell</h2>
<p>The speech service provides a well documented API that can easily be called using PowerShell’s native <code class="highlighter-rouge">Invoke-RestMethod</code> command. The required information is available on Microsoft Docs (link below), all I had to do is wrap a little PowerShell around it and I had created a quick module. You can install the module using the following command:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Install-Module PSSpeech
</code></pre></div></div>
<p>Before we can call any of the speech service’s API endpoints, we have to use the API key to get a token and store it in a variable for later use:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Get-SpeechToken -Key yourapikey | Save-SpeechToken
</code></pre></div></div>
<p>Now we should be able to get a list of available voices using <code class="highlighter-rouge">Get-SpeechVoicesList | Format-Table</code>.</p>
<p>And finally we can convert some input text to speech using one of the voices from the list:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Convert-TextToSpeech -Voice en-US-JessaNeural -Text "Hi Tom, I'm Jessa from Azure!" -Path jessa.mp3
Convert-TextToSpeech -Voice en-GB-HarryNeural -Text "Hi Tom, I'm Harry from Azure!" -Path harry.mp3
</code></pre></div></div>
<p>You can find a lot of information about the speech service in the links below, be sure to check out the SSML structure to see how you can customize the voices, introduce pauses to the audio file, and many other things.</p>
<p>You can find the code for the module in my GitHub, please let me know if you find it useful and feel free to submit a pull request with your optimizations :)</p>
<p>This is the first post in this new year, best wishes and thanks for reading!</p>
<p>Tom</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://azure.microsoft.com/en-us/services/cognitive-services/speech-services/">Speech Services</a></li>
<li><a href="https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/speech-synthesis-markup">Speech Synthesis Markup Language (SSML)</a></li>
<li><a href="https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/rest-text-to-speech">Text to Speech REST API</a></li>
<li><a href="https://ntsystems.it/PowerShell/PSSpeech/">PSSpeech Module</a></li>
</ul>Tom TorgglerConfiguring policy-based QoS for Teams with Intune2019-11-29T19:12:06+01:002019-11-29T19:12:06+01:00repo://posts.collection/_posts/2019-11-29-configuring-policy-based-qos-for-teams-with-intune.md<p>Traditional Active Directory with group policy has no place in the big-picture of the modern workplace, so we need a novel solution to apply policy-based QoS to our Teams clients.<!-- more --> One could argue that QoS has no place in the modern workplace either, but that’s a discussion for another day.</p>
<h2 id="configuration-service-provider">Configuration Service Provider</h2>
<p>So a CSP or configuration service provider is pretty much exactly what everyone with some traditional enterprise IT background would expect from a group policy object, but delivered from the cloud and, at least in theory, applicable to various types of devices. According to Microsoft Docs it is “an interface to read, set, modify, or delete configuration settings on the device. These settings map to registry keys or files.”</p>
<p>You can find a link to the CSP reference below.</p>
<h2 id="networkqospolicy-csp">NetworkQoSPolicy CSP</h2>
<p>Now it turns out there is a CSP for policy-based QoS but it just applies to Surface Hub devices. If you’re lucky enough to configure QoS on such a device, here is a screenshot of the settings you will most likely use.</p>
<blockquote title="Warning">
<p>The port numbers may be different in your environment.</p>
</blockquote>
<figure>
<a href="/assets/2019/11-29-01.png">
<img src="/assets/2019/11-29-01.png" alt="" />
</a>
</figure>
<h2 id="msft_netqospolicysettingdata">MSFT_NetQosPolicySettingData</h2>
<p>So here we are trying to configure QoS settings on our Windows 10 clients but CSPs are of no great help. Luckily we can use PowerShell to configure policy-based QoS and Intune provides an easy way to deploy PowerShell scripts to our clients.</p>
<p>To configure Windows 10 to tag packets sent by the Teams.exe and on the configured source ports for each modality, we could use three simple commands like in the example below:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">New-NetQosPolicy</span><span class="w"> </span><span class="nt">-NetworkProfile</span><span class="w"> </span><span class="nx">All</span><span class="w"> </span><span class="nt">-AppPathNameMatchCondition</span><span class="w"> </span><span class="nx">Teams.exe</span><span class="w"> </span><span class="nt">-IPSrcPortStartMatchCondition</span><span class="w"> </span><span class="nx">50020</span><span class="w"> </span><span class="nt">-IPSrcPortEndMatchCondition</span><span class="w"> </span><span class="nx">50039</span><span class="w"> </span><span class="nt">-DSCPValue</span><span class="w"> </span><span class="nx">46</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"Teams Audio"</span><span class="w">
</span><span class="n">New-NetQosPolicy</span><span class="w"> </span><span class="nt">-NetworkProfile</span><span class="w"> </span><span class="nx">All</span><span class="w"> </span><span class="nt">-AppPathNameMatchCondition</span><span class="w"> </span><span class="nx">Teams.exe</span><span class="w"> </span><span class="nt">-IPSrcPortStartMatchCondition</span><span class="w"> </span><span class="nx">50400</span><span class="w"> </span><span class="nt">-IPSrcPortEndMatchCondition</span><span class="w"> </span><span class="nx">50059</span><span class="w"> </span><span class="nt">-DSCPValue</span><span class="w"> </span><span class="nx">34</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"Teams Video"</span><span class="w">
</span><span class="n">New-NetQosPolicy</span><span class="w"> </span><span class="nt">-NetworkProfile</span><span class="w"> </span><span class="nx">All</span><span class="w"> </span><span class="nt">-AppPathNameMatchCondition</span><span class="w"> </span><span class="nx">Teams.exe</span><span class="w"> </span><span class="nt">-IPSrcPortStartMatchCondition</span><span class="w"> </span><span class="nx">50069</span><span class="w"> </span><span class="nt">-IPSrcPortEndMatchCondition</span><span class="w"> </span><span class="nx">50070</span><span class="w"> </span><span class="nt">-DSCPValue</span><span class="w"> </span><span class="nx">28</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"Teams AppSharing"</span><span class="w">
</span></code></pre></div></div>
<p>You can find a link to the cmdlet reference for <code class="highlighter-rouge">New-NetQosPolicy</code> below.</p>
<p>Save the above commands to a file with ps1 extension and head over to <a href="https://endpoint.microsoft.com/#blade/Microsoft_Intune_DeviceSettings/DevicesMenu/powershell">endpoint.microsoft.com</a>. Create a new script for Windows 10, upload the the ps1 file and set it to run in system context and using the 64 bit PowerShell host. Now assign the script to a group that contains your devices.</p>
<figure>
<a href="/assets/2020/08-14 223129.png">
<img src="/assets/2020/08-14 223129.png" alt="" />
</a>
</figure>
<p>Once the script was applied you can use <code class="highlighter-rouge">Get-NetQosPolicy</code> to verify the policies were applied correctly.</p>
<h2 id="teams-meeting-settings">Teams Meeting Settings</h2>
<p>For the above configuration to make any sense, we first have to specify a port range for each modality in the Microsoft Teams admin center.</p>
<p>You can find a link to the Teams admin center below.</p>
<p>The following screenshot shows an example configuration where a distinct port range is used for each type of traffic, this allows us to distinguish the traffic types and apply different DSCP tags using policy-based QoS.</p>
<figure>
<a href="/assets/2019/11-29-02.png">
<img src="/assets/2019/11-29-02.png" alt="" />
</a>
</figure>
<p>Special thanks to Mr. Workplace Expert <a href="https://twitter.com/WengerDave">Dave Wenger</a>! Check out his blog in the links below.</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://docs.microsoft.com/en-us/windows/client-management/mdm/configuration-service-provider-reference">Configuration service provider reference</a></li>
<li><a href="https://docs.microsoft.com/en-us/powershell/module/netqos/new-netqospolicy">New-NetQosPolicy</a></li>
<li><a href="https://admin.teams.microsoft.com/meetings/settings">Microsoft Teams admin center</a></li>
<li><a href="https://blog.contoso-bern.ch/">https://blog.contoso-bern.ch/</a></li>
</ul>Tom Torggler