<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://singularvector.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://singularvector.github.io/" rel="alternate" type="text/html" /><updated>2026-10-23T20:34:29+00:00</updated><id>https://singularvector.github.io/feed.xml</id><title type="html">SingularVector</title><subtitle>Systems engineering and infrastructure automation by Daniele: 20+ years across Windows, Linux and identity, with practical PowerShell guides and tools.</subtitle><author><name>SingularVector</name><email>PsCustomObject@outlook.com</email></author><entry><title type="html">PowerShell String Encryption in IT-ToolBox 3.0</title><link href="https://singularvector.github.io/powershell/howto/PowerShell-String-Encryption-IT-ToolBox/" rel="alternate" type="text/html" title="PowerShell String Encryption in IT-ToolBox 3.0" /><published>2026-10-23T00:00:00+00:00</published><updated>2026-10-23T00:00:00+00:00</updated><id>https://singularvector.github.io/powershell/howto/PowerShell-String-Encryption-IT-ToolBox</id><content type="html" xml:base="https://singularvector.github.io/powershell/howto/PowerShell-String-Encryption-IT-ToolBox/"><![CDATA[<h2 id="same-function-names-different-behaviour">Same function names, different behaviour</h2>

<p>In my earlier article on <a href="/powershell/howto/Store-Credentials-in-PowerShell-Script/">storing credentials in PowerShell</a>, I used two functions from my <strong>IT-ToolBox</strong> module: <code class="language-plaintext highlighter-rouge">New-StringEncryption</code> and <code class="language-plaintext highlighter-rouge">New-StringDecryption</code>.</p>

<p>Those functions are still part of the module, but version 3.0 changes how they work. The names remain the same. The parameters, encryption format and handling of older data do not.</p>

<p>In this post I will go through the current implementation, show a couple of examples and explain what needs attention if you already have encrypted strings created with the older functions.</p>

<p>The examples target <strong>IT-ToolBox 3.0.0</strong>, which requires <strong>PowerShell 7.4 or later</strong> and a platform with .NET AES-GCM support. They do not apply to Windows PowerShell 5.1 or the legacy functions.</p>

<h2 id="what-changed">What changed</h2>

<p>The older functions used <code class="language-plaintext highlighter-rouge">RijndaelManaged</code>, with optional passphrase and salt parameters that defaulted to the computer name. They also accepted a caller-supplied vector, with a fixed value used when it was omitted.</p>

<p>That computer-name default could make an encrypted value appear tied to a particular machine. It was not a machine access restriction, though. The name was an input to the key derivation: reproducing the same inputs elsewhere could reproduce the key.</p>

<p>Version 3.0 removes those defaults. A passphrase is required, and the function generates the salt and nonce internally for each encryption operation.</p>

<p>The implementation uses <strong>AES-256-GCM</strong>, with a key derived through <strong>PBKDF2-HMAC-SHA256 at 600,000 iterations</strong>. Decryption verifies an authentication tag before returning the string. An incorrect passphrase or a modified authenticated payload causes an exception.</p>

<p>This matters because recovering some text is not enough to establish that encrypted data has not changed. The new format includes that check.</p>

<h2 id="encrypting-and-decrypting-a-string">Encrypting and decrypting a string</h2>

<p>First, obtain the module using the <a href="https://github.com/SingularVector/IT-ToolBox">repository setup instructions</a>, then import the version used in this article:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Import-Module</span><span class="w"> </span><span class="nx">IT-ToolBox</span><span class="w"> </span><span class="nt">-RequiredVersion</span><span class="w"> </span><span class="nx">3.0.0</span><span class="w">
</span></code></pre></div></div>

<p>For this interactive example I use <code class="language-plaintext highlighter-rouge">Get-Credential</code> to enter the encryption passphrase without putting it directly in the command history. The username is only a label here; it is not used for encryption.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$passphraseInput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Credential</span><span class="w"> </span><span class="nt">-UserName</span><span class="w"> </span><span class="s1">'string-encryption'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Message</span><span class="w"> </span><span class="s1">'Enter the encryption passphrase in the password field'</span><span class="w">

</span><span class="nv">$encryptionPassphrase</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$passphraseInput</span><span class="o">.</span><span class="nf">GetNetworkCredential</span><span class="p">()</span><span class="o">.</span><span class="nf">Password</span><span class="w">
</span></code></pre></div></div>

<p>The functions accept ordinary strings. Calling <code class="language-plaintext highlighter-rouge">GetNetworkCredential().Password</code> therefore creates a plaintext string in memory. The prompt avoids a hard-coded passphrase; it does not change the function’s memory-handling limits.</p>

<p>Now we can encrypt a value:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Demonstration data, not a real password.</span><span class="w">
</span><span class="nv">$valueToProtect</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Example configuration value'</span><span class="w">

</span><span class="nv">$encryptedValue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-StringEncryption</span><span class="w"> </span><span class="nt">-StringToEncrypt</span><span class="w"> </span><span class="nv">$valueToProtect</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-EncryptPassPhrase</span><span class="w"> </span><span class="nv">$encryptionPassphrase</span><span class="w">

</span><span class="nv">$encryptedValue</span><span class="w">
</span></code></pre></div></div>

<p>The result starts with <code class="language-plaintext highlighter-rouge">ITTB1.</code> followed by a Base64-encoded payload. Your output will differ each time, even when the input string and passphrase are unchanged.</p>

<p>To recover the original value, pass the complete result back to the companion function:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$decryptedValue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-StringDecryption</span><span class="w"> </span><span class="nt">-EncryptedString</span><span class="w"> </span><span class="nv">$encryptedValue</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-EncryptPassPhrase</span><span class="w"> </span><span class="nv">$encryptionPassphrase</span><span class="w">

</span><span class="nv">$decryptedValue</span><span class="w"> </span><span class="o">-ceq</span><span class="w"> </span><span class="nv">$valueToProtect</span><span class="w">
</span><span class="c"># True</span><span class="w">
</span></code></pre></div></div>

<p>The ciphertext can be stored separately and loaded by another script. The passphrase must be supplied through a separate, appropriate secret-handling mechanism. Saving both together in the same script would defeat the reason for separating them.</p>

<p>This prompt is for an interactive demonstration. An unattended job needs a way to obtain its passphrase without waiting for a person.</p>

<h2 id="what-is-in-the-encrypted-value">What is in the encrypted value</h2>

<p>The <code class="language-plaintext highlighter-rouge">ITTB1.</code> prefix identifies the format. The decoded payload contains:</p>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Size</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Salt</td>
      <td>16 bytes</td>
    </tr>
    <tr>
      <td>Nonce</td>
      <td>12 bytes</td>
    </tr>
    <tr>
      <td>Ciphertext</td>
      <td>Same byte length as the UTF-8 plaintext</td>
    </tr>
    <tr>
      <td>Authentication tag</td>
      <td>16 bytes</td>
    </tr>
  </tbody>
</table>

<p>The salt and nonce are stored with the ciphertext so that decryption has the inputs it needs. The passphrase is not included.</p>

<p>The format also fixes the derivation settings. Callers no longer supply <code class="language-plaintext highlighter-rouge">EncryptSalt</code> or <code class="language-plaintext highlighter-rouge">IntersectingVector</code>, and the decoder does not try different historical formats until one happens to produce output.</p>

<p>The implementation limits plaintext to <strong>1 MiB of UTF-8 data</strong> and the passphrase to <strong>4,096 characters</strong>. These functions handle strings; they are not a file-encryption interface.</p>

<h2 id="checking-the-failure-path">Checking the failure path</h2>

<p>A successful round trip is useful, but I also want to know what happens when decryption should fail.</p>

<p>Using the value created above, try a different passphrase:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">try</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="n">New-StringDecryption</span><span class="w"> </span><span class="nt">-EncryptedString</span><span class="w"> </span><span class="nv">$encryptedValue</span><span class="w"> </span><span class="se">`
</span><span class="w">        </span><span class="nt">-EncryptPassPhrase</span><span class="w"> </span><span class="p">(</span><span class="nv">$encryptionPassphrase</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">'-different'</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">[</span><span class="n">System.Security.Cryptography.CryptographicException</span><span class="p">]</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Output</span><span class="w"> </span><span class="s1">'Decryption rejected the incorrect passphrase.'</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>For this example, use a passphrase comfortably below the maximum length so that the altered value remains within the parameter limit.</p>

<p>The module’s <a href="https://github.com/SingularVector/IT-ToolBox/blob/5ea85e41a0672847e70716342651af52179ac649/Tests/StringEncryption.Tests.ps1">string-encryption tests</a> cover more than this: Unicode and multiline strings, empty input, fresh salts and nonces, incorrect passphrases, and changes to the salt, nonce, ciphertext and tag. They also check malformed input and size limits.</p>

<p>There is an independent fixture generated with Python’s cryptography library. That gives the tests a value produced outside these two functions, rather than relying entirely on an encrypt/decrypt pair that could share the same mistake.</p>

<h2 id="migrating-existing-encrypted-strings">Migrating existing encrypted strings</h2>

<p>An old encrypted value cannot be passed directly to the version 3.0 decoder. The new function rejects historical ciphertext rather than silently falling back to the old implementation.</p>

<p>The original functions remain in the repository’s <strong>Legacy</strong> directory for recovering existing data. They are not imported by the current module.</p>

<p>The migration sequence is:</p>

<ol>
  <li>Preserve the original ciphertext and identify the inputs used to create it.</li>
  <li>In a separate session, use the original decoder with the original passphrase, salt and vector. If the call used computer-name defaults, you need the original computer name.</li>
  <li>Verify the recovered value against a trusted reference or its intended use. The legacy format does not authenticate its contents, so successful decryption alone does not establish integrity.</li>
  <li>Encrypt the recovered value with the modern function and a strong, separately protected passphrase.</li>
  <li>Verify the new value before replacing the original data or changing the consuming script.</li>
</ol>

<p>Do not load the legacy and modern functions into the same session and assume the function name tells you which implementation is running. They deliberately retain the same names. The <a href="https://github.com/SingularVector/IT-ToolBox#legacy-migration">module’s migration notes</a> describe the compatibility boundary.</p>

<h2 id="where-i-would-use-this">Where I would use this</h2>

<p>These functions provide a way to encrypt and authenticate a string when the application already has an appropriate way to manage the passphrase.</p>

<p>They do not decide who should be allowed to decrypt it. Anyone with the ciphertext and the correct passphrase can use a compatible implementation to recover the value. There is no Windows user or originating-machine restriction in the modern format.</p>

<p>The code clears temporary plaintext and key byte arrays, but managed strings cannot be reliably erased. The input value, passphrase and returned plaintext remain subject to that limitation. Converting the returned value to <code class="language-plaintext highlighter-rouge">SecureString</code> does not undo the plaintext exposure that has already occurred.</p>

<p>For service authentication, I would first check whether the service supports a workload identity, managed identity or certificate-based application authentication. When a password is required, a secret store may be the better place to manage access, rotation and retrieval. String encryption is useful, but the script still needs an answer to the question: where does its decryption passphrase come from?</p>

<p>The implementation, tests and version-specific documentation are available in my <strong><a href="https://github.com/SingularVector/IT-ToolBox">IT-ToolBox repository</a></strong>. If you already use the older functions, check the migration notes before updating the module.</p>]]></content><author><name>SingularVector</name><email>PsCustomObject@outlook.com</email></author><category term="PowerShell" /><category term="HowTo" /><category term="PowerShell" /><category term="IT-ToolBox" /><category term="Encryption" /><category term="Automation" /><summary type="html"><![CDATA[Using New-StringEncryption and New-StringDecryption in IT-ToolBox 3.0, with explicit passphrases, authenticated encryption and a migration path for older data.]]></summary></entry><entry><title type="html">Getting Started with .gitignore</title><link href="https://singularvector.github.io/git/version-control/getting-started-with-gitignore/" rel="alternate" type="text/html" title="Getting Started with .gitignore" /><published>2025-05-04T00:00:00+00:00</published><updated>2025-05-04T00:00:00+00:00</updated><id>https://singularvector.github.io/git/version-control/getting-started-with-gitignore</id><content type="html" xml:base="https://singularvector.github.io/git/version-control/getting-started-with-gitignore/"><![CDATA[<blockquote>
  <p>🧰 This post kicks off a new series on Git essentials—starting with <code class="language-plaintext highlighter-rouge">.gitignore</code>, a simple yet powerful tool for managing what gets tracked in your repositories.</p>
</blockquote>

<hr />

<h2 id="️-what-is-gitignore">🗃️ What is <code class="language-plaintext highlighter-rouge">.gitignore</code>?</h2>

<p>When working with Git, it’s common to have files that shouldn’t be tracked—such as build artifacts, temporary files, or sensitive information. The <code class="language-plaintext highlighter-rouge">.gitignore</code> file allows you to specify patterns for files and directories that Git should ignore.</p>

<p>By placing a <code class="language-plaintext highlighter-rouge">.gitignore</code> file in your repository’s root directory, you instruct Git to disregard specified files, keeping your version history clean and focused.</p>

<hr />

<h2 id="-creating-a-gitignore-file">📝 Creating a <code class="language-plaintext highlighter-rouge">.gitignore</code> File</h2>

<p>To create a <code class="language-plaintext highlighter-rouge">.gitignore</code> file:</p>

<ol>
  <li>Navigate to your repository’s root directory.</li>
  <li>
    <p>Create the <code class="language-plaintext highlighter-rouge">.gitignore</code> file:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch</span> .gitignore
</code></pre></div>    </div>
  </li>
  <li>Open the file in your preferred text editor and add patterns for files/directories to ignore.</li>
</ol>

<p>Example:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Ignore node_modules directory</span>
node_modules/

<span class="c"># Ignore all .log files</span>
<span class="k">*</span>.log

<span class="c"># Ignore build output</span>
dist/
</code></pre></div></div>

<hr />

<h2 id="-applying-gitignore-to-already-tracked-files">🔄 Applying <code class="language-plaintext highlighter-rouge">.gitignore</code> to Already Tracked Files</h2>

<p>If you’ve already committed files that should be ignored, updating <code class="language-plaintext highlighter-rouge">.gitignore</code> won’t remove them from the repository. To stop tracking these files:</p>

<ol>
  <li>
    <p>Remove the files from the index:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git <span class="nb">rm</span> <span class="nt">--cached</span> filename
</code></pre></div>    </div>
  </li>
  <li>
    <p>Commit the changes:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git commit <span class="nt">-m</span> <span class="s2">"Remove ignored files from tracking"</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Push the changes to your remote repository.</p>
  </li>
</ol>

<hr />

<h2 id="-global-gitignore">🌐 Global <code class="language-plaintext highlighter-rouge">.gitignore</code></h2>

<p>For patterns that should apply to all your Git repositories (e.g., OS-specific files like <code class="language-plaintext highlighter-rouge">.DS_Store</code>), you can set up a global <code class="language-plaintext highlighter-rouge">.gitignore</code>:</p>

<ol>
  <li>
    <p>Create a global <code class="language-plaintext highlighter-rouge">.gitignore</code> file:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch</span> ~/.gitignore_global
</code></pre></div>    </div>
  </li>
  <li>
    <p>Configure Git to use this file:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git config <span class="nt">--global</span> core.excludesFile ~/.gitignore_global
</code></pre></div>    </div>
  </li>
</ol>

<p>Then add your global ignore patterns to <code class="language-plaintext highlighter-rouge">~/.gitignore_global</code>.</p>

<hr />

<h2 id="-tips-and-best-practices">🧪 Tips and Best Practices</h2>

<ul>
  <li><strong>Use comments</strong>: Prefix lines with <code class="language-plaintext highlighter-rouge">#</code> to explain ignore rules.</li>
  <li><strong>Be specific</strong>: Avoid overly broad patterns that may unintentionally ignore important files.</li>
  <li><strong>Leverage templates</strong>: Use <a href="https://github.com/github/gitignore">GitHub’s official <code class="language-plaintext highlighter-rouge">.gitignore</code> templates</a> for popular languages, editors, and frameworks.</li>
</ul>

<hr />

<p>By effectively using <code class="language-plaintext highlighter-rouge">.gitignore</code>, you maintain a clean and efficient repository, free from unnecessary files and potential security risks.</p>

<p>Stay tuned for the next post in this series, where we’ll explore branching strategies and how to manage them effectively.</p>]]></content><author><name>SingularVector</name><email>PsCustomObject@outlook.com</email></author><category term="git" /><category term="version-control" /><category term="git" /><category term="gitignore" /><category term="version-control" /><category term="best-practices" /><summary type="html"><![CDATA[🧰 This post kicks off a new series on Git essentials—starting with .gitignore, a simple yet powerful tool for managing what gets tracked in your repositories.]]></summary></entry><entry><title type="html">🚀 Launching My Cloud &amp;amp; DevOps Notes Hub</title><link href="https://singularvector.github.io/devops/notes/launching-my-cloud-devops-notes-hub/" rel="alternate" type="text/html" title="🚀 Launching My Cloud &amp;amp; DevOps Notes Hub" /><published>2025-05-02T00:00:00+00:00</published><updated>2025-05-02T00:00:00+00:00</updated><id>https://singularvector.github.io/devops/notes/launching-my-cloud-devops-notes-hub</id><content type="html" xml:base="https://singularvector.github.io/devops/notes/launching-my-cloud-devops-notes-hub/"><![CDATA[<p>Cloud infrastructure and infrastructure as code are part of my wider systems engineering work. This companion notebook brings together technical references, study plans and hands-on experiments, building on more than 20 years across Windows, Linux, identity and automation.</p>

<p>🎉 <strong>It’s live here</strong>: <a href="https://singularvector.github.io/terraforming-my-career/">Terraforming My Career</a></p>

<hr />

<h2 id="-what-youll-find">📘 What You’ll Find</h2>

<p>At launch in May 2025, the notebook included:</p>

<ul>
  <li>🛠 <strong>Terraform</strong>: from getting started to change management and teardown</li>
  <li>📚 <strong>Learning Plans</strong>: structured study alongside practical infrastructure work</li>
  <li>🐳 Sections for <strong>Docker</strong>, <strong>Kubernetes</strong>, and <strong>AWS</strong> (in progress)</li>
  <li>📖 <strong>Reading Notes</strong> from books like <em>Terraform: Up &amp; Running</em></li>
</ul>

<p>Everything is structured in folders, rendered with <a href="https://just-the-docs.github.io/">Just the Docs</a>, and completely searchable.</p>

<hr />

<h2 id="-why-im-doing-this">🔍 Why I’m Doing This</h2>

<p>Publishing engineering notes helps me:</p>

<ul>
  <li>Keep technical references and lab findings organised</li>
  <li>Share approaches and tradeoffs with other engineers</li>
  <li>Build writing discipline and clarity of thought</li>
</ul>

<p>Over time, this space will grow into a personal knowledge base and lab journal — and you’re welcome to follow along.</p>

<hr />

<p>💡 <em>Want to build something similar?</em>
Check out the <a href="https://github.com/SingularVector/terraforming-my-career">GitHub repo</a>, fork it, and feel free to reach out.</p>]]></content><author><name>SingularVector</name><email>PsCustomObject@outlook.com</email></author><category term="DevOps" /><category term="Notes" /><category term="Cloud" /><category term="DevOps" /><category term="Terraform" /><category term="GitHub Pages" /><category term="JustTheDocs" /><summary type="html"><![CDATA[A companion infrastructure notebook covering Terraform, cloud automation and technical references, built on my systems engineering experience.]]></summary></entry><entry><title type="html">SSH Connection Manager in Command Line</title><link href="https://singularvector.github.io/linux/howto/SSH-Connection-Manager/" rel="alternate" type="text/html" title="SSH Connection Manager in Command Line" /><published>2024-09-20T00:00:00+00:00</published><updated>2024-09-20T00:00:00+00:00</updated><id>https://singularvector.github.io/linux/howto/SSH-Connection-Manager</id><content type="html" xml:base="https://singularvector.github.io/linux/howto/SSH-Connection-Manager/"><![CDATA[<h2 id="managing-ssh-connections">Managing SSH Connections</h2>

<p>In the Windows ecosystem, tools like Remote Desktop Manager help handle and manage multiple connections to the various servers within the infrastructure. But how can we achieve something similar for SSH connections to frequently accessed servers in Linux?</p>

<p>Let’s explore how this can be done efficiently in the command line.</p>

<h2 id="ssh-config-file">SSH Config file</h2>

<p>First, if it doesn’t already exist, create the .ssh directory in a convenient location (I keep mine in my $HOME directory):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> .ssh
</code></pre></div></div>

<p>Next, create a file named config, adjust its default permissions, and open it with your favorite text editor:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>config

<span class="nb">chmod </span>600 config

vim config
</code></pre></div></div>

<p>This file is automatically sourced by the terminal, allowing you to easily connect to machines defined in it. Here’s a sample of the configuration I use to manage my servers:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host node_01
        Hostname node01.lab.com
        User pscustomobject
        IdentityFile /home/pscustomobject/.ssh/id_rsa
Host node_02
        Hostname node02.example.com
        User adminuser
        Port 4242

<span class="c">## Set connection defaults for all hosts, this is overriden by host options</span>
Host <span class="k">*</span>
     ForwardAgent no
     ForwardX11 no
     ForwardX11Trusted <span class="nb">yes
     </span>User pscustomobject
     Port 22
     Protocol 2
     ServerAliveInterval 60
     ServerAliveCountMax 30

</code></pre></div></div>

<h2 id="key-notes">Key Notes</h2>

<ul>
  <li>The IdentityFile directive points to the location of your private key. This can be stored in a shared or networked location for easier access across multiple devices.</li>
  <li>The *Host ** section defines defaults for all hosts, which individual host entries can override.</li>
</ul>

<p>Once the SSH configuration is in place, you can connect to any of the defined hosts simply by issuing the command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh node_01
</code></pre></div></div>

<p>Better yet, tab completion will work just as it does for standard commands, so you don’t have to remember the exact name of each node.</p>

<h2 id="additional-tips">Additional Tips</h2>

<p>I highly recommend reading the <em>ssh_config</em> man page (<em>man ssh_config</em>) to discover the numerous other options you can use in your config file to further streamline and simplify SSH connection management.</p>]]></content><author><name>SingularVector</name><email>PsCustomObject@outlook.com</email></author><category term="Linux" /><category term="Howto" /><category term="Linux" /><category term="Tips" /><category term="HowTo" /><summary type="html"><![CDATA[Managing multiple SSH connections and hosts can sometimes be problematic. In this post, I will show how you can easily save multiple connections directly in the terminal (Linux) to take advantage of tab completion, passwordless authentication, and more.]]></summary></entry><entry><title type="html">PowerShell - Generate Unique UPN</title><link href="https://singularvector.github.io/powershell/howto/PowerShell-Generate-Unique-Upn/" rel="alternate" type="text/html" title="PowerShell - Generate Unique UPN" /><published>2024-07-27T00:00:00+00:00</published><updated>2024-07-27T00:00:00+00:00</updated><id>https://singularvector.github.io/powershell/howto/PowerShell-Generate-Unique-Upn</id><content type="html" xml:base="https://singularvector.github.io/powershell/howto/PowerShell-Generate-Unique-Upn/"><![CDATA[<h2 id="generating-unique-user-principal-names-upns-in-powershell">Generating Unique User Principal Names (UPNs) in PowerShell</h2>

<p>When managing Active Directory (AD) environments, creating unique User Principal Names (UPNs) is a common, and challenging!, task.</p>

<p>This blog post covers two PowerShell functions that help ensure UPN uniqueness: <strong>Test-UPNExist</strong> and <strong>Get-UniqueUPN</strong>. These functions can be particularly useful for automating user creation processes and can be integrated into your automation projects and solutions.</p>

<p><strong>Note</strong> Both cmdlets are still under active development so do expect some changes, as I progress with development I will update the post. Feel free to report any issue, idea or suggestion so that I can integrate it into the final cmdlet version.</p>

<h3 id="function-1-test-upnexist">Function 1: Test-UPNExist</h3>

<p>This is a support function that is used to check if given UPN already exists in the AD environment.</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">Test-UPNExist</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="cm">&lt;#
    </span><span class="cs">.SYNOPSIS</span><span class="cm">
        Cmdlet will check if a given UPN exists in the forest.
    
    </span><span class="cs">.DESCRIPTION</span><span class="cm">
        Cmdlet is a diagnostic tool to check if a given UPN is already assigned to a user in the forest.
    
    </span><span class="cs">.PARAMETER</span><span class="cm"> UPN
        A string representing the UPN to check for uniqueness.
    
    </span><span class="cs">.PARAMETER</span><span class="cm"> AdServer
        A string representing the name of the domain controller to be used for the check, if parameter
        is not specified the closest Global Catalog is used.
    
    </span><span class="cs">.EXAMPLE</span><span class="cm">
        PS C:\&gt; Test-UPNExist -UPN 'John.Doe@example.com'
#&gt;</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="w">
    </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">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateNotNullOrEmpty</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">$UPN</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateNotNullOrEmpty</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">$AdServer</span><span class="w">
    </span><span class="p">)</span><span class="w">
    
    </span><span class="kr">if</span><span class="w"> </span><span class="p">([</span><span class="n">string</span><span class="p">]::</span><span class="n">IsNullOrEmpty</span><span class="p">(</span><span class="nv">$AdServer</span><span class="p">)</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="nv">$adForest</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">System.DirectoryServices.ActiveDirectory.Forest</span><span class="p">]::</span><span class="n">GetCurrentForest</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">$ldapPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'{0}{1}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="s1">'GC://'</span><span class="p">,</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nv">$adForest</span><span class="o">.</span><span class="nf">FindGlobalCatalog</span><span class="p">()</span><span class="o">.</span><span class="nf">Name</span><span class="p">)</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">else</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">$ldapPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'{0}{1}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="s1">'LDAP://'</span><span class="p">,</span><span class="w"> </span><span class="nv">$AdServer</span><span class="w">
    </span><span class="p">}</span><span class="w">
    
    </span><span class="c"># Instantiate required objects and run query</span><span class="w">
    </span><span class="nv">$adDomain</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.DirectoryServices.DirectoryEntry</span><span class="p">(</span><span class="nv">$ldapPath</span><span class="p">)</span><span class="w">
    </span><span class="nv">$adSearcher</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.DirectoryServices.DirectorySearcher</span><span class="p">(</span><span class="nv">$adDomain</span><span class="p">)</span><span class="w">
    </span><span class="nv">$adSearcher</span><span class="o">.</span><span class="nf">SearchScope</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Subtree'</span><span class="w">
    </span><span class="nv">$adSearcher</span><span class="o">.</span><span class="nf">PageSize</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1000</span><span class="w">
    </span><span class="nv">$adSearcher</span><span class="o">.</span><span class="nf">Filter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"(&amp;(objectCategory=person)(userPrincipalName=</span><span class="nv">$UPN</span><span class="s2">))"</span><span class="w">
    </span><span class="p">[</span><span class="n">void</span><span class="p">](</span><span class="nv">$adSearcher</span><span class="o">.</span><span class="nf">PropertiesToLoad</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"userPrincipalName"</span><span class="p">))</span><span class="w">
    
    </span><span class="p">[</span><span class="n">array</span><span class="p">]</span><span class="nv">$searchResult</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$adSearcher</span><span class="o">.</span><span class="nf">FindOne</span><span class="p">()</span><span class="w">
    
    </span><span class="kr">return</span><span class="w"> </span><span class="bp">$null</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="nv">$searchResult</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Here’s a summary explanation of the parameters:</p>

<ul>
  <li><strong>Parameters</strong>
    <ul>
      <li><em>$UPN</em> - A string representing the UPN to check</li>
      <li><em>$Server</em> -  A string representing the name of the  LDAP server to query. Parameter is optional and if omitted function will automatically select the closest global catalog server</li>
    </ul>
  </li>
</ul>

<p>Here’s an example usage:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$upnExists</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-UPNExist</span><span class="w"> </span><span class="nt">-UPN</span><span class="w"> </span><span class="s2">"john.doe@example.com"</span><span class="w">

</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$upnExists</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w"> </span><span class="c"># Redundant I know but I like to make code redeable :-)</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"The UPN exists."</span><span class="w">
</span><span class="p">}</span><span class="w"> 
</span><span class="kr">else</span><span class="w"> 
</span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"The UPN does not exist."</span><span class="w">
</span><span class="p">}</span><span class="w">

</span></code></pre></div></div>

<h3 id="function-2-get-uniqueupn">Function 2: Get-UniqueUPN</h3>

<p>This function generates a unique UPN based on given name components and ensures it doesn’t already exist in the AD forest.</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-UniqueUPN</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="cm">&lt;#
    </span><span class="cs">.SYNOPSIS</span><span class="cm">
        Cmdlet will generate a forest wide unique UPN.
    
    </span><span class="cs">.DESCRIPTION</span><span class="cm">
        Cmdlet will generate a forest wide unique UPN according to generation rules
        defined by the user.
        
        Cmdlet accept different types of objects to generate the UPN to allow greater flexibility
        
        ADObject - For example and object from Get-AdUser cmdlet
        Strings - Representing First Name, Last Name etc.
        DirectoryService Objects - For example when using native .Net methods to retrieve the identity
    
    </span><span class="cs">.PARAMETER</span><span class="cm"> ADObject
        An ADObject for example output of the Get-ADUser cmdlet
    
    </span><span class="cs">.PARAMETER</span><span class="cm"> FirstName
        A string representing the First Name of the user
    
    </span><span class="cs">.PARAMETER</span><span class="cm"> LastName
        A string representing the Last Name of the user
    
    </span><span class="cs">.PARAMETER</span><span class="cm"> MiddleName
        A string representing the Middle Name of the user, parameter is optional.
    
    </span><span class="cs">.PARAMETER</span><span class="cm"> UPNSuffix
        A string representing the UPN suffix to be used.
    
    </span><span class="cs">.PARAMETER</span><span class="cm"> FirstNameFormat
        A string representing the format to be for the First Name part of the UPN.
    
    </span><span class="cs">.PARAMETER</span><span class="cm"> LastNameFormat
        A string representing the format to be for the Last Name part of the UPN.
    
    </span><span class="cs">.PARAMETER</span><span class="cm"> IncludeMiddleName
        When paramenter is specified user Middle Name, if present, will be included in the UPN generation process.
    
    </span><span class="cs">.PARAMETER</span><span class="cm"> ADServer
        A string representing the name of the AD Domain Controller that will be used to query Active Directory.
    
        If no server is specified the closest Global Catalog will be automatically selected.
    
    </span><span class="cs">.PARAMETER</span><span class="cm"> Separator
        A string representing the separator to be used between UPN parts, defaults to a '.'.
#&gt;</span><span class="w">
    
    </span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">(</span><span class="bp">DefaultParameterSetName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Strings'</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">Parameter</span><span class="p">(</span><span class="n">ParameterSetName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'ADObject'</span><span class="p">,</span><span class="w">
                   </span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">object</span><span class="p">]</span><span class="nv">$ADObject</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">ParameterSetName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Strings'</span><span class="p">,</span><span class="w">
                   </span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateNotNullOrEmpty</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">$FirstName</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">ParameterSetName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Strings'</span><span class="p">,</span><span class="w">
                   </span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateNotNullOrEmpty</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">$LastName</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">ParameterSetName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Strings'</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateNotNullOrEmpty</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">$MiddleName</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">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateNotNullOrEmpty</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">$UPNSuffix</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s1">'FullName'</span><span class="p">,</span><span class="w"> </span><span class="s1">'FirstLetter'</span><span class="p">,</span><span class="w"> </span><span class="n">IgnoreCase</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateNotNullOrEmpty</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">$FirstNameFormat</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Full'</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s1">'FullName'</span><span class="p">,</span><span class="w"> </span><span class="s1">'FirstLetter'</span><span class="p">,</span><span class="w"> </span><span class="n">IgnoreCase</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateNotNullOrEmpty</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">$LastNameFormat</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'FullName'</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$IncludeMiddleName</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateNotNullOrEmpty</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">$ADServer</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateNotNullOrEmpty</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">$Separator</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'.'</span><span class="w">
    </span><span class="p">)</span><span class="w">
    
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="bp">$PSCmdlet</span><span class="o">.</span><span class="nf">ParameterSetName</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'ADObject'</span><span class="p">)</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="kr">switch</span><span class="w"> </span><span class="p">(</span><span class="nv">$ADObject</span><span class="o">.</span><span class="nf">GetType</span><span class="p">()</span><span class="o">.</span><span class="nf">FullName</span><span class="p">)</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="s1">'Microsoft.ActiveDirectory.Management.ADUser'</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">$firstName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ADObject</span><span class="o">.</span><span class="nf">GivenName</span><span class="w">
                </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$lastName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ADObject</span><span class="o">.</span><span class="nf">Surname</span><span class="w">
                </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$middleName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ADObject</span><span class="o">.</span><span class="nf">MiddleName</span><span class="w">
                
                </span><span class="kr">break</span><span class="w">
            </span><span class="p">}</span><span class="w">
            </span><span class="s1">'System.DirectoryServices.DirectoryEntry'</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">$firstName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ADObject</span><span class="o">.</span><span class="n">Properties</span><span class="p">[</span><span class="s1">'givenName'</span><span class="p">][</span><span class="mi">0</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">$lastName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ADObject</span><span class="o">.</span><span class="n">Properties</span><span class="p">[</span><span class="s1">'sn'</span><span class="p">][</span><span class="mi">0</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">$middleName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ADObject</span><span class="o">.</span><span class="n">Properties</span><span class="p">[</span><span class="s1">'middleName'</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span><span class="w">
                
                </span><span class="kr">break</span><span class="w">
            </span><span class="p">}</span><span class="w">
            </span><span class="s1">'System.DirectoryServices.SearchResult'</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">$firstName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ADObject</span><span class="o">.</span><span class="n">Properties</span><span class="p">[</span><span class="s1">'givenName'</span><span class="p">][</span><span class="mi">0</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">$lastName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ADObject</span><span class="o">.</span><span class="n">Properties</span><span class="p">[</span><span class="s1">'sn'</span><span class="p">][</span><span class="mi">0</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">$middleName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ADObject</span><span class="o">.</span><span class="n">Properties</span><span class="p">[</span><span class="s1">'middleName'</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span><span class="w">
                
                </span><span class="kr">break</span><span class="w">
            </span><span class="p">}</span><span class="w">
            </span><span class="n">default</span><span class="w">
            </span><span class="p">{</span><span class="w">
                </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"Unsupported AD object type: </span><span class="si">$(</span><span class="nv">$ADObject</span><span class="o">.</span><span class="nf">GetType</span><span class="p">()</span><span class="o">.</span><span class="nf">FullName</span><span class="si">)</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="p">}</span><span class="w">
    </span><span class="kr">else</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">$firstName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$FirstName</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$lastName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$LastName</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$middleName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$MiddleName</span><span class="w">
    </span><span class="p">}</span><span class="w">
    
    </span><span class="c"># Format first name</span><span class="w">
    </span><span class="nv">$firstName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">switch</span><span class="w"> </span><span class="p">(</span><span class="nv">$FirstNameFormat</span><span class="p">)</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="s1">'FullName'</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nv">$firstName</span><span class="w">
        </span><span class="p">}</span><span class="w">
        </span><span class="s1">'FirstLetter'</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nv">$firstName</span><span class="o">.</span><span class="nf">Substring</span><span class="p">(</span><span class="nx">0</span><span class="p">,</span><span class="w"> </span><span class="nx">1</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"># Format last name</span><span class="w">
    </span><span class="nv">$LastName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">switch</span><span class="w"> </span><span class="p">(</span><span class="nv">$FirstNameFormat</span><span class="p">)</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="s1">'FullName'</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nv">$LastName</span><span class="w">
        </span><span class="p">}</span><span class="w">
        </span><span class="s1">'FirstLetter'</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nv">$LastName</span><span class="o">.</span><span class="nf">Substring</span><span class="p">(</span><span class="nx">0</span><span class="p">,</span><span class="w"> </span><span class="nx">1</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"># Use middle name</span><span class="w">
    </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$middleNamePart</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$IncludeMiddleName</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="nv">$MiddleName</span><span class="p">)</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="s1">'{0}{1}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="nv">$Separator</span><span class="p">,</span><span class="w"> </span><span class="nv">$MiddleName</span><span class="w">
    </span><span class="p">}</span><span class="w">
    
    </span><span class="c"># Setup required attributes</span><span class="w">
    </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$baseUPN</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="s1">'{0}{1}{2}{3}@{4}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="nv">$FirstName</span><span class="p">,</span><span class="w"> </span><span class="nv">$middleNamePart</span><span class="p">,</span><span class="w"> </span><span class="nv">$Separator</span><span class="p">,</span><span class="w"> </span><span class="nv">$LastName</span><span class="p">,</span><span class="w"> </span><span class="nv">$UPNSuffix</span><span class="p">)</span><span class="o">.</span><span class="nf">ToLower</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">$uniqueUPN</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$baseUPN</span><span class="w">
    </span><span class="p">[</span><span class="n">int</span><span class="p">]</span><span class="nv">$counter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="w">
    
    </span><span class="kr">while</span><span class="w"> </span><span class="p">(</span><span class="n">Test-UPNExist</span><span class="w"> </span><span class="nt">-UPN</span><span class="w"> </span><span class="nv">$uniqueUPN</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="nv">$ADServer</span><span class="p">)</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="nv">$uniqueUPN</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'{0}{1}@{2}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="p">(</span><span class="nv">$baseUPN</span><span class="o">.</span><span class="nf">Split</span><span class="p">(</span><span class="s1">'@'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]),</span><span class="w"> </span><span class="nv">$counter</span><span class="p">,</span><span class="w"> </span><span class="nv">$UPNSuffix</span><span class="w">
        
        </span><span class="nv">$counter</span><span class="o">++</span><span class="w">
    </span><span class="p">}</span><span class="w">
    
    </span><span class="kr">return</span><span class="w"> </span><span class="nv">$uniqueUPN</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Here’s a summary explanation of the parameters:</p>

<ul>
  <li><strong>Parameters</strong>
    <ul>
      <li><em>$ADObject</em> - We can pass an existing AD object to the function, for example output from the <em>Get-AdUser</em> cmdlet</li>
      <li><em>$FirstName</em> - A string representing the <em>First Name</em> of a given user</li>
      <li><em>$LastName</em> - A string representing the <em>Last Name</em> of a given user</li>
      <li><em>MiddleName</em> - A string representing the <em>Middle Name</em> of a given user (of course optional)</li>
      <li><em>$UPNSuffix</em> - A string representing the domain suffix for the UPN</li>
      <li><em>FirstNameFormat</em> - The format of the first name in the UPN (e.g. Full or First letter of name)</li>
      <li><em>IncludeMiddleName</em> - Switch parameter indicating to cmdlet if Middle Name should be used or not in the UPN generation</li>
      <li><em>$Server</em> - A string representing the name of the  LDAP server to query. Parameter is optional and if omitted function will automatically select the closest global catalog server</li>
      <li><em>$Separator</em> - The separator to use between the name parts. If not specified a ‘.’ (dot) is used</li>
    </ul>
  </li>
</ul>

<p>Below you can find couple examples of functions at work</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Using string parameters</span><span class="w">
</span><span class="nv">$uniqueUPN</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-UniqueUPN</span><span class="w"> </span><span class="nt">-FirstName</span><span class="w"> </span><span class="s2">"John"</span><span class="w"> </span><span class="nt">-LastName</span><span class="w"> </span><span class="s2">"Doe"</span><span class="w"> </span><span class="nt">-UPNSuffix</span><span class="w"> </span><span class="s2">"example.com"</span><span class="w"> </span><span class="nt">-FirstNameFormat</span><span class="w"> </span><span class="s2">"FirstLetter"</span><span class="w"> </span><span class="nt">-IncludeMiddleName</span><span class="w"> </span><span class="nt">-MiddleName</span><span class="w"> </span><span class="s2">"A"</span><span class="w">

</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Generated UPN: </span><span class="nv">$uniqueUPN</span><span class="s2">"</span><span class="w">

</span><span class="c"># Using an AD object</span><span class="w">
</span><span class="nv">$adUser</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADUser</span><span class="w"> </span><span class="nt">-Identity</span><span class="w"> </span><span class="s2">"johndoe"</span><span class="w">
</span><span class="nv">$uniqueUPN</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-UniqueUPN</span><span class="w"> </span><span class="nt">-ADObject</span><span class="w"> </span><span class="nv">$adUser</span><span class="w"> </span><span class="nt">-UPNSuffix</span><span class="w"> </span><span class="s2">"example.com"</span><span class="w">

</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Generated UPN: </span><span class="nv">$uniqueUPN</span><span class="s2">"</span><span class="w">
</span></code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>By using <strong>Test-UPNExist</strong> and <strong>Get-UniqueUPN</strong>, you can automate the process of generating unique UPNs for users in your Active Directory environment.</p>

<p>These functions ensure that each UPN is unique and adheres to the naming conventions you specify.</p>

<p>This approach minimizes the risk of conflicts and simplifies user management.</p>

<p>As mentioned both functions are still under development and I need to cleanup code and optimize some parts of it but the core functionality is there.</p>

<p>As usual all the code is available on my <a href="https://github.com/SingularVector/PowerShell-Functions">GitHub repository</a>.</p>

<p>I would love to receive feedback, ideas or implementation ideas for them!</p>]]></content><author><name>SingularVector</name><email>PsCustomObject@outlook.com</email></author><category term="PowerShell" /><category term="Howto" /><category term="PowerShell" /><category term="Tips" /><category term="HowTo" /><summary type="html"><![CDATA[In this short post I will share two new cmdlets I have developed to generate forest wide unique UPN handling the different edge cases.]]></summary></entry><entry><title type="html">PowerShell - Get day of weekday</title><link href="https://singularvector.github.io/powershell/howto/PowerShell-Get-Day-Of-Week-Number/" rel="alternate" type="text/html" title="PowerShell - Get day of weekday" /><published>2022-02-21T00:00:00+00:00</published><updated>2022-02-21T00:00:00+00:00</updated><id>https://singularvector.github.io/powershell/howto/PowerShell-Get-Day-Of-Week-Number</id><content type="html" xml:base="https://singularvector.github.io/powershell/howto/PowerShell-Get-Day-Of-Week-Number/"><![CDATA[<h2 id="powershell-get-current-date">PowerShell Get current date</h2>

<p>PowerShell natively supports getting the current, or specific, date via the following cmdlet:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Date</span><span class="w">
</span></code></pre></div></div>

<p>Which, by default, will produce the following output:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Monday</span><span class="p">,</span><span class="w"> </span><span class="nx">February</span><span class="w"> </span><span class="nx">21</span><span class="p">,</span><span class="w"> </span><span class="nx">2022</span><span class="w"> </span><span class="nx">10:11:39</span><span class="w"> </span><span class="nx">PM</span><span class="w">
</span></code></pre></div></div>

<p>Cmdlet supports methods which can be used to display/return the day of the week for the selected date</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">DayOfWeek</span><span class="w">
</span></code></pre></div></div>

<p>There are situations where rather than returning a string with the day name it can be useful to return the <strong>number</strong> (1 to 7) associated with the day’s name, this can easily be accomplished with the following command</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">DayOfWeek</span><span class="o">.</span><span class="nf">value__</span><span class="w">

</span><span class="c"># Output</span><span class="w">
</span><span class="mi">1</span><span class="w">
</span></code></pre></div></div>

<p>This is especially handy when inserting data into SQL database supporting dates as <em>TinyInt</em> data types.</p>]]></content><author><name>SingularVector</name><email>PsCustomObject@outlook.com</email></author><category term="PowerShell" /><category term="Howto" /><category term="PowerShell" /><category term="Tips" /><category term="HowTo" /><summary type="html"><![CDATA[In this short post we will see how we can use PowerShell to get an integer representing the number associated with the day of the week for the current date.]]></summary></entry><entry><title type="html">Exchange Online Management Module - Could not use the certificate for signing</title><link href="https://singularvector.github.io/powershell/exchange/office%20365/Could-not-use-the-certificate-for-signing/" rel="alternate" type="text/html" title="Exchange Online Management Module - Could not use the certificate for signing" /><published>2022-01-27T00:00:00+00:00</published><updated>2022-01-27T00:00:00+00:00</updated><id>https://singularvector.github.io/powershell/exchange/office%20365/Could-not-use-the-certificate-for-signing</id><content type="html" xml:base="https://singularvector.github.io/powershell/exchange/office%20365/Could-not-use-the-certificate-for-signing/"><![CDATA[<h2 id="exchange-online-certificate-based-authentication">Exchange Online Certificate Based authentication</h2>

<p>As I have written in my <a href="https://singularvector.github.io/powershell/exchange/office%20365/Cannot-bind-argument-to-parameter-Token-Expiry/">previous post about TokenExpiry error message Microsoft is retiring ability to connect to Exchange Online via basic authentication</a>.</p>

<p>You can read my article on how to implement <em>Certificate Based authentication</em> for Exchange Online <a href="https://singularvector.github.io/powershell/office365/exchange/Exchange-Online-Certificate-Based-Authentication/">here</a>.</p>

<h2 id="could-not-use-the-certificate-for-signing-error-message">Could not use the certificate for signing error message</h2>

<p>Today while I was updating code for one of our automations I created a request for a new certificate to use for authentication purposes.</p>

<p>Once I deployed code to our test environment automation was failing the connection to Exchange Online with the following error</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">System.Management.Automation.RuntimeException</span><span class="p">]</span><span class="w"> </span><span class="n">One</span><span class="w"> </span><span class="nx">or</span><span class="w"> </span><span class="nx">more</span><span class="w"> </span><span class="nx">errors</span><span class="w"> </span><span class="nx">occurred.</span><span class="w">
</span><span class="p">[</span><span class="n">Microsoft.Identity.Client.MsalClientException</span><span class="p">]</span><span class="w"> </span><span class="n">Could</span><span class="w"> </span><span class="nx">not</span><span class="w"> </span><span class="nx">use</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">certificate</span><span class="w"> </span><span class="nx">for</span><span class="w"> </span><span class="nx">signing.</span><span class="w"> </span><span class="nx">See</span><span class="w"> </span><span class="nx">inner</span><span class="w"> </span><span class="nx">exception</span><span class="w"> </span><span class="nx">for</span><span class="w"> </span><span class="nx">details.</span><span class="w"> </span><span class="nx">Possible</span><span class="w"> </span><span class="nx">cause:</span><span class="w"> </span><span class="nx">this</span><span class="w"> </span><span class="nx">may</span><span class="w"> </span><span class="nx">be</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="nx">known</span><span class="w"> </span><span class="nx">issue</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">apps</span><span class="w"> </span><span class="nx">build</span><span class="w"> </span><span class="nx">against</span><span class="w"> </span><span class="o">.</span><span class="nf">NET</span><span class="w"> </span><span class="nx">Desktop</span><span class="w"> </span><span class="nx">4.6</span><span class="w"> </span><span class="nx">or</span><span class="w"> </span><span class="nx">lower.</span><span class="w"> </span><span class="nx">Either</span><span class="w"> </span><span class="nx">target</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="nx">higher</span><span class="w"> </span><span class="nx">version</span><span class="w"> </span><span class="nx">of</span><span class="w"> </span><span class="o">.</span><span class="nf">NET</span><span class="w"> </span><span class="nx">desktop</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nx">4.6.1</span><span class="w"> </span><span class="nx">and</span><span class="w"> </span><span class="nx">above</span><span class="p">,</span><span class="w"> </span><span class="nx">or</span><span class="w"> </span><span class="nx">use</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="nx">different</span><span class="w"> </span><span class="nx">certificate</span><span class="w"> </span><span class="nx">type</span><span class="w"> </span><span class="p">(</span><span class="n">non-CNG</span><span class="p">)</span><span class="w"> </span><span class="n">or</span><span class="w"> </span><span class="nx">sign</span><span class="w"> </span><span class="nx">your</span><span class="w"> </span><span class="nx">own</span><span class="w"> </span><span class="nx">assertion</span><span class="w"> </span><span class="nx">as</span><span class="w"> </span><span class="nx">described</span><span class="w"> </span><span class="nx">at</span><span class="w"> </span><span class="nx">aka.ms/msal-net-signed-assertion.</span><span class="w">
</span><span class="p">[</span><span class="n">System.Security.Cryptography.CryptographicException</span><span class="p">]</span><span class="w"> </span><span class="n">Invalid</span><span class="w"> </span><span class="nx">provider</span><span class="w"> </span><span class="nx">type</span><span class="w"> </span><span class="nx">specified.</span><span class="w">
</span></code></pre></div></div>

<p>Funnily enough the same certificate and cmdlets were working fine with PowerShell 7.</p>

<p>After quite some troubleshooting I’ve found out the problem was caused by the certificate’s private key using <a href="https://docs.microsoft.com/en-us/mem/configmgr/core/plan-design/network/cng-certificates-overview"><em>Cryptography Next Generate (CNG)</em></a> template rather than RSA.</p>

<p>Not having direct access to the CA releasing the certificate I could not change this so I had to resort on either running the automation in PowerShell 7 or update the certificate itself.</p>

<p>Luckily this is easily done via OpenSSL. Let’s see how.</p>

<h2 id="convert-certificate-private-key-from-cng-to-rsa">Convert Certificate private key from CNG to RSA</h2>

<p>If you have installed Git, cygwin or Windows Subsystem for Linux you just need to fire a bash prompt and use the following commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Extract the public key from the cert </span>
OpenSSL pkcs12 <span class="nt">-in</span> <span class="s2">"CNGCertificate.pfx"</span> <span class="nt">-nokeys</span> <span class="nt">-out</span> <span class="s2">"temp.cer"</span>

<span class="c"># Extract the private key</span>
OpenSSL pkcs12 <span class="nt">-in</span> <span class="s2">"CNGCertificate.pfx"</span> <span class="nt">-nocerts</span> <span class="nt">-out</span> <span class="s2">"temp.pem"</span>

<span class="c"># Convert key to RSA</span>
OpenSSL rsa <span class="nt">-inform</span> PEM <span class="nt">-in</span> <span class="s2">"temp.pem"</span> <span class="nt">-out</span> <span class="s2">"temp.rsa"</span>

<span class="c"># Finally create a new pfx file</span>
OpenSSL pkcs12 <span class="nt">-export</span> <span class="nt">-in</span> <span class="s2">"temp.cer"</span> <span class="nt">-inkey</span> <span class="s2">"temp.rsa"</span> <span class="nt">-out</span> <span class="s2">"RSACertificate.pfx"</span>
</code></pre></div></div>

<p class="notice--warning"><strong>Note:</strong> In the above commands I am not using a password for the certificate as everything is local to my machine but a password is definitely <em>required</em> when exporting a certificate together with the private key.</p>

<p>Once the new pfx file has been created all <em>temporary</em> certificates can be safely removed from the system and connection to Exchange Online will go through just fine.</p>

<p>Again if you can use PowerShell 7 you will not face this issue but in case you’re stuck with version 5.1 and facing this error message hopefully this post can save you some headaches.</p>

<p>Full credit for the solution goes to this <a href="https://stackoverflow.com/questions/22581811/invalid-provider-type-specified-cryptographicexception-when-trying-to-load-pri/34103154#34103154">StackOverFlow thread</a></p>]]></content><author><name>SingularVector</name><email>PsCustomObject@outlook.com</email></author><category term="PowerShell" /><category term="Exchange" /><category term="Office 365" /><category term="PowerShell" /><category term="Office365" /><category term="Exchange" /><summary type="html"><![CDATA[When trying to establish a connection to Exchange Online Could not use the certificate for signing error message is displayed.]]></summary></entry><entry><title type="html">Cannot bind argument to parameter TokenExpiryTime because it is null - Error Message</title><link href="https://singularvector.github.io/powershell/exchange/office%20365/Cannot-bind-argument-to-parameter-Token-Expiry/" rel="alternate" type="text/html" title="Cannot bind argument to parameter TokenExpiryTime because it is null - Error Message" /><published>2022-01-25T00:00:00+00:00</published><updated>2022-01-25T00:00:00+00:00</updated><id>https://singularvector.github.io/powershell/exchange/office%20365/Cannot%20bind%20argument%20to%20parameter-Token-Expiry</id><content type="html" xml:base="https://singularvector.github.io/powershell/exchange/office%20365/Cannot-bind-argument-to-parameter-Token-Expiry/"><![CDATA[<h2 id="exchange-online-certificate-based-authentication">Exchange Online Certificate Based authentication</h2>

<p>Microsoft is, <em>finally</em>, disabling <strong>basic authentication</strong> (read username and password) in Exchange Online in favor of <strong>Certificate Based authentication</strong>.</p>

<p>Once this change is fully implemented, around mid February at least for some tenants, connecting via username and passwords to Exchange Online will not be possible anymore.</p>

<p>You can read my article on how to implement <em>Certificate Based authentication</em> for Exchange Online <a href="https://singularvector.github.io/powershell/office365/exchange/Exchange-Online-Certificate-Based-Authentication/">here</a>.</p>

<p>As a result of this change I started updating one of our automations, responsible for the whole life-cycle of our mailboxes, to ditch old credential objects in favor of the more secure Certificate Authentication.</p>

<p>This is when I encountered the <em>cannot bind argument to parameter ‘token expiry time’ because it is null.</em> error message.</p>

<h2 id="multiple-powershell-exchange-sessions">Multiple PowerShell Exchange Sessions</h2>

<p>When operating an hybrid environment it is pretty common to open, in the same window/session, a PowerShell connection to both Exchange on-prem and Exchange Online.</p>

<p>This is required as part of the configuration, usually creation of the mailbox, takes place in on-prem for example via the <em>New-RemoteMailbox</em> cmdlet while other parts of the configuration are performed directly online for example when delegating mailbox permissions.</p>

<p>While debugging my workflow I have noticed that, while trying to retreive mailbox information from the on-prem server, an exception was being thrown</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Cmdlet I was running</span><span class="w">
</span><span class="n">Get-RemoteMailbox</span><span class="w"> </span><span class="nt">-Identity</span><span class="w"> </span><span class="nv">$userUpn</span><span class="w">

</span><span class="c"># Part of the exception message</span><span class="w">
</span><span class="n">Cannot</span><span class="w"> </span><span class="nx">bind</span><span class="w"> </span><span class="nx">argument</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">parameter</span><span class="w"> </span><span class="s1">'token expiry time'</span><span class="w"> </span><span class="nx">because</span><span class="w"> </span><span class="nx">it</span><span class="w"> </span><span class="nx">is</span><span class="w"> </span><span class="nx">null.</span><span class="w">
</span></code></pre></div></div>

<p>It took me a bit to figure this out as no exception was thrown during the connection <em>phase</em> either nor there was any other obvious pointer.</p>

<p>When I was about to give up and open a ticket with Microsoft, which is usually as helpful as freezer in the North Pole, I discovered that establishing a connection to Exchange Online followed by a connection to the on-prem server was yielding the desired result. In my workflow I had this the other way around, first local Exchange and then Online service, which was causing the issue.</p>

<p class="notice--warning"><strong>Note:</strong> I have experienced/tested this with version 2.0.4 and 2.0.5 of Exchange Online PowerShell module but other versions could be affected as well.</p>

<p>I did not dig deep into the root cause of the issue but plan to do this tomorrow and already sent my feedback to exocmdletpreview {at} service {dot} microsoft {dot} com but I doubt I will hear anything from that channel. 
I will anyhow open a ticket with support to at least have an official statement/clarification on this.</p>

<p>As soon as I have any news I will update the post until then I hope you can find the information useful.</p>]]></content><author><name>SingularVector</name><email>PsCustomObject@outlook.com</email></author><category term="PowerShell" /><category term="Exchange" /><category term="Office 365" /><category term="PowerShell" /><category term="Office365" /><category term="Exchange" /><summary type="html"><![CDATA[When opening a PowerShell session to both on-prem and Exchange online in the same window cannot bing argument to parameter TokenExpiryTime because is null error message could be displayed. Let's explore how to solve this.]]></summary></entry><entry><title type="html">Setup an internal PowerShell Repository with NuGet Step by Step</title><link href="https://singularvector.github.io/powershell/howto/Setup-Internal-PowerShell-Repository/" rel="alternate" type="text/html" title="Setup an internal PowerShell Repository with NuGet Step by Step" /><published>2021-06-02T00:00:00+00:00</published><updated>2021-06-02T00:00:00+00:00</updated><id>https://singularvector.github.io/powershell/howto/Setup-Internal-PowerShell-Repository</id><content type="html" xml:base="https://singularvector.github.io/powershell/howto/Setup-Internal-PowerShell-Repository/"><![CDATA[<h2 id="powershell-repositories">PowerShell repositories</h2>

<p>If you follow my blog you probably know my <a href="https://github.com/SingularVector">GitHub</a> where I share modules like the <a href="https://github.com/SingularVector/IT-ToolBox">IT-ToolBox</a> or handy standalone <a href="https://github.com/SingularVector/PowerShell-Functions">Functions</a> that I developed to solve specific issues.</p>

<p>In my posts I often talked about <a href="https://singularvector.github.io/powershell/tips/PowerShell-Restore-Default-Repository/">PowerShell Gallery</a> which you probably already used to install a module released by Microsoft or other developers.</p>

<p>Both are great tools when sharing code with a large audience over the internet, GitHub Private repositories are scope of this article, but a large chunk of my work involves developing <em>modules</em> that me and my colleagues user internally for automation purposes. These modules usually leverage technologies that are available only in my work environment and sometimes implement functions that would not have much sense outside my company.</p>

<p>One of the main issues we faced when initially started development of our own modules is ease of distribution to other team members for this reason we’ve setup internal <strong>NuGet</strong> repositories which can be used an <em>internal</em> PowerShell Gallery.</p>

<h2 id="required-tools">Required tools</h2>

<p>Here’s what you will need to follow this article and setup your own PowerShell repository:</p>

<ul>
  <li>A copy of <strong><a href="https://visualstudio.microsoft.com/downloads/">Visual Studio</a></strong> the community edition will do just fine and it’s free</li>
  <li>The ASP.NET and web development workload installed <a href="https://docs.microsoft.com/en-us/visualstudio/install/modify-visual-studio?view=vs-2019">here’s</a> a guide on how to modify an existing Visual Studio installation in case you don’t have the appropriate workload installed already</li>
  <li>A server running a supported copy of <strong>Windows Server</strong> I am using Windows 2019 but 2016 will do just fine</li>
</ul>

<p><strong>Note:</strong> If you do not have or cannot install Visual Studio at the end of the post I have posted a link to my <a href="https://github.com/SingularVector/Nuget-PowerShell-Repository/tree/main/NuGetRepository/NuGetRepository">GitHub Repository</a></p>

<h2 id="create-the-visual-studio-project">Create the Visual Studio Project</h2>

<p>Once all requirements are in place open Visual Studio and create a project using the <em>ASP.Web Application (Visual C#)</em> template.</p>

<figure>
  <a href="https://singularvector.github.io//assets/images/NugetArticle/VS_Create_New_Project.png">
  <img src="/assets/images/NugetArticle/VS_Create_New_Project.png" /></a>
</figure>

<figure>
  <a href="https://singularvector.github.io//assets/images/NugetArticle/VS_Web_Application.png">
  <img src="/assets/images/NugetArticle/VS_Web_Application.png" /></a>
</figure>

<p>Application name is not important just avoid <em>NuGet</em> as this would create a conflict with one of the <em>packages</em> we need to install as part of the dependencies, in my example I’ve used <strong>NuGetRepository</strong> but it can really be anything</p>

<figure>
  <a href="https://singularvector.github.io//assets/images/NugetArticle/Application_Configuration.png">
  <img src="/assets/images/NugetArticle/Application_Configuration.png" /></a>
</figure>

<p>In the screen that will appear simply select the <em>Empty</em> template and then the <em>Create</em> button</p>

<figure>
  <a href="https://singularvector.github.io//assets/images/NugetArticle/Project_Configuration.png">
  <img src="/assets/images/NugetArticle/Project_Configuration.png" /></a>
</figure>

<p>Once project has finished loading/being created <em>right-click</em> on the project name in <em>solution explorer</em> and select <strong>Manage NuGet Packages</strong></p>

<figure>
  <a href="https://singularvector.github.io//assets/images/NugetArticle/Manage_NuGet_Package.png">
  <img src="/assets/images/NugetArticle/Manage_NuGet_Package.png" /></a>
</figure>

<p>In the <em>NuGetRepository</em> window select the <em>Browse</em> tab and search for package <strong>NuGet.Server</strong> and click on the <strong>Install</strong> button.</p>

<figure>
  <a href="https://singularvector.github.io//assets/images/NugetArticle/Install_NuGet_Component.png">
  <img src="/assets/images/NugetArticle/Install_NuGet_Component.png" /></a>
</figure>

<p>Keep I am using the latest stable release available at the time of this writing but you can select a different version of the package suiting your needs.</p>

<p>This step will take some time, depending on your connection speed, and a window showing you changes to the solution and asking your confirmation will appear in which case simply click on the <em>Ok</em> button.</p>

<p>Once the installation step is complete I highly recommend to change the build type from <em>Debug</em>, default for new projects, to <em>Release</em> as this will disable all debug logging that is otherwise enabled for the solution.</p>

<figure>
  <a href="https://singularvector.github.io//assets/images/NugetArticle/Set_Build_Type.png">
  <img src="/assets/images/NugetArticle/Set_Build_Type.png" /></a>
</figure>

<p>Once this is done go to <em>Build / Build Solution</em> menu, or press F6 if you’re lazy like me, to <em>package</em> together all files making up the solution.</p>

<p>This will create a folder structure, under the path previously specified, containing all files required by the solution. Project path is visible in the <strong>Properties</strong> window under the <em>Project Folder</em> field</p>

<figure>
  <a href="https://singularvector.github.io//assets/images/NugetArticle/Project_Path.png">
  <img src="/assets/images/NugetArticle/Project_Path.png" /></a>
</figure>

<p><strong>Note:</strong> By default solution will be build under the C:\Users\username\source\repos\AppName\AppName\ path</p>

<h2 id="install-required-server-components">Install required Server Components</h2>

<p>Once the solution has been build in Visual Studio move on the server that will be hosting the repository and launch the following cmdlet from an elevated PowerShell session to install required <em>IIS</em> components</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Splatted version</span><span class="w">
</span><span class="nv">$paramInstallWindowsFeature</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">Name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Web-Server'</span><span class="p">,</span><span class="w"> </span><span class="s1">'Web-Net-Ext45'</span><span class="p">,</span><span class="w"> </span><span class="s1">'Web-Asp-Net45'</span><span class="p">,</span><span class="w"> </span><span class="s1">'Web-ISAPI-Filter'</span><span class="p">,</span><span class="w"> </span><span class="s1">'Web-ISAPI-Ext'</span><span class="w">
    </span><span class="nx">IncludeManagementTools</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">Install-WindowsFeature</span><span class="w"> </span><span class="err">@</span><span class="nx">paramInstallWindowsFeature</span><span class="w">

</span><span class="c"># Long unsplatted version</span><span class="w">
</span><span class="n">Install-WindowsFeature</span><span class="w"> </span><span class="nx">Web-Server</span><span class="p">,</span><span class="nx">Web-Net-Ext45</span><span class="p">,</span><span class="nx">Web-Asp-Net45</span><span class="p">,</span><span class="nx">Web-ISAPI-Filter</span><span class="p">,</span><span class="nx">Web-ISAPI-Ext</span><span class="w"> </span><span class="nt">-IncludeManagementTools</span><span class="w">
</span></code></pre></div></div>

<p>Once all components are in place copy the solution files to a directory on the server with the default being <em>C:\inetpub\wwwroot</em>. Path is not really import and could be anything you’d like, I will just stick with the default.</p>

<p>As a reference here’s how the content of my application folder looks like on my test server</p>

<figure>
  <a href="https://singularvector.github.io//assets/images/NugetArticle/IIS_Application.png">
  <img src="/assets/images/NugetArticle/IIS_Application.png" /></a>
</figure>

<h3 id="update-webconfig-configuration-file">Update Web.Config configuration file</h3>

<p>The <strong>web.config</strong> file is located in the project root folder, assuming default path this will be <em>C:\inetpub\wwwroot\NuGetRepository</em>, open it with a text editor and under the <strong>&lt;system.web&gt;</strong> section you should see line similar the following (actual .Net Framework version could vary)</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;system.web&gt;</span>
    <span class="nt">&lt;compilation</span> <span class="na">debug=</span><span class="s">"true"</span> <span class="na">targetFramework=</span><span class="s">"4.6.1"</span> <span class="nt">/&gt;</span>
    <span class="c">&lt;!-- maxRequestLength is specified in Kb --&gt;</span>
    <span class="nt">&lt;httpRuntime</span> <span class="na">targetFramework=</span><span class="s">"4.6.1"</span> <span class="na">maxRequestLength=</span><span class="s">"30720"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;compilation</span> <span class="na">debug=</span><span class="s">"true"</span> <span class="na">targetFramework=</span><span class="s">"4.6"</span> <span class="nt">/&gt;</span>
  <span class="nt">&lt;/system.web&gt;</span>
</code></pre></div></div>

<p>Comment out or delete one of the instances of <strong>&lt;compilation&gt;</strong> tag so that resultant file will be similar the following</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;system.web&gt;</span>
    <span class="nt">&lt;compilation</span> <span class="na">debug=</span><span class="s">"true"</span> <span class="na">targetFramework=</span><span class="s">"4.6.1"</span> <span class="nt">/&gt;</span>
    <span class="c">&lt;!-- maxRequestLength is specified in Kb --&gt;</span>
    <span class="nt">&lt;httpRuntime</span> <span class="na">targetFramework=</span><span class="s">"4.6.1"</span> <span class="na">maxRequestLength=</span><span class="s">"30720"</span> <span class="nt">/&gt;</span>
  <span class="nt">&lt;/system.web&gt;</span>
</code></pre></div></div>

<p><strong>Note:</strong> If the <em>web.config</em> file is not updated when opening the IIS Application an <em>Internal Server Error</em> message will be displayed an application won’t work.</p>

<h2 id="deploy-application-to-iis">Deploy Application to IIS</h2>

<p>We can finally deploy the <em>NuGet</em> application to IIS to do so open **IIS Manager / Expand <Server Name=""> / Sites** right-click on Default Web Site and select **Add Application**</Server></p>

<figure>
  <a href="https://singularvector.github.io//assets/images/NugetArticle/Add_Application.png">
  <img src="/assets/images/NugetArticle/Add_Application.png" /></a>
</figure>

<p>In the window that appears provide a name (alias) for the application and specify the path where application files reside</p>

<figure>
  <a href="https://singularvector.github.io//assets/images/NugetArticle/Application_Alias.png">
  <img src="/assets/images/NugetArticle/Application_Alias.png" /></a>
</figure>

<p>To test the configuration point your browser to <strong>http://localhost/Nuget</strong> which should yield the following page</p>

<figure>
  <a href="https://singularvector.github.io//assets/images/NugetArticle/Nuget_Page.png">
  <img src="/assets/images/NugetArticle/Nuget_Page.png" /></a>
</figure>

<h3 id="configure-package-directory">Configure Package Directory</h3>

<p>In the default configuration packages are stored under the same path where the application lives in the <em>Packages</em> directory, in our example <em>C:\inetpub\wwwroot\Nuget\Packages</em>. This can be changed in the <em>web.config</em> configuration file via the following tag</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c">&lt;!--
    Change the path to the packages folder. Default is ~/Packages.
    This can be a virtual or physical path.
    --&gt;</span>
    <span class="nt">&lt;add</span> <span class="na">key=</span><span class="s">"packagesPath"</span> <span class="na">value=</span><span class="s">"C:\SomePath\Packages"</span> <span class="nt">/&gt;</span>
</code></pre></div></div>

<h3 id="configure-repository-api-key">Configure Repository API Key</h3>

<p>Up to this point the repository is ready and can be used to <em>download</em> packages from it but if we need to <em>upload</em> data, in our example PowerShell modules, to it we would need some form of authentication which in NuGet is accomplished through an API key.</p>

<p>First of all generate a unique API key, I generally use a GUID generated via <em>New-Guid</em> cmdlet, open the <em>web.config</em> file and update the <strong>apiKey</strong> value as in the following example</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c">&lt;!--
    Determines if an Api Key is required to push\delete packages from the server.
    --&gt;</span>
    <span class="nt">&lt;add</span> <span class="na">key=</span><span class="s">"requireApiKey"</span> <span class="na">value=</span><span class="s">"true"</span> <span class="nt">/&gt;</span>
    <span class="c">&lt;!--
    Set the value here to allow people to push/delete packages from the server.
    NOTE: This is a shared key (password) for all users.
    --&gt;</span>
    <span class="nt">&lt;add</span> <span class="na">key=</span><span class="s">"apiKey"</span> <span class="na">value=</span><span class="s">"&lt;GUID VALUE&gt;"</span> <span class="nt">/&gt;</span>
</code></pre></div></div>

<p>If you want to disable authentication all together you can simply set the <strong>requireApiKey</strong> to <em>false</em>.</p>

<h2 id="closing-notes">Closing notes</h2>

<p>This guide should set you in the right path for configuring your own internal PowerShell module repository but this is just the tip of the iceberg.</p>

<p>In the next articles I will explain how to register a repository and push modules to it.</p>

<h2 id="github-link">GitHub Link</h2>

<p>I have created a GitHub repository where I’ve posted both the package and the full Visual Studio Solution so you can simply download the files and be up and running in no time.</p>

<ul>
  <li><a href="https://github.com/SingularVector/Nuget-PowerShell-Repository/tree/main/NuGetRepository/NuGetRepository">This is the direct link to the application files</a> that you can simply copy over to IIS</li>
  <li><a href="https://github.com/SingularVector/Nuget-PowerShell-Repository">This is the link to the base repository</a> containing readme, changelog etc.</li>
</ul>

<p>I will keep this repository up to date when new versions of NuGet are released.</p>]]></content><author><name>SingularVector</name><email>PsCustomObject@outlook.com</email></author><category term="PowerShell" /><category term="HowTo" /><category term="PowerShell" /><category term="Tips" /><summary type="html"><![CDATA[In this article I will guide you through the creation, configuration and deployment of an internal NuGet repository that can be used to easily distribute and share PowerShell modules in the internal network.]]></summary></entry><entry><title type="html">Get current user UPN</title><link href="https://singularvector.github.io/powershell/tips/howto/Get-Current-User-UPN/" rel="alternate" type="text/html" title="Get current user UPN" /><published>2021-05-28T00:00:00+00:00</published><updated>2021-05-28T00:00:00+00:00</updated><id>https://singularvector.github.io/powershell/tips/howto/Get-Current-User-UPN</id><content type="html" xml:base="https://singularvector.github.io/powershell/tips/howto/Get-Current-User-UPN/"><![CDATA[<p>A few days ago while developing a cmdlet for an internal module in support to a larger automation workflow I found myself in need to easily derive <em>UserPrincipalName</em> of the currently logged on user running the command.</p>

<p>If you look  this up in your favorite search engine chances are you came across results like the following (there are more but these are the more common <em>suggestions</em> from users):</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1</span><span class="w">
</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">UserName</span><span class="w">

</span><span class="c"># 2</span><span class="w">
</span><span class="p">[</span><span class="n">System.Security.Principal.WindowsIdentity</span><span class="p">]::</span><span class="n">GetCurrent</span><span class="p">()</span><span class="o">.</span><span class="nf">Name</span><span class="w">

</span><span class="c"># 3</span><span class="w">
</span><span class="p">[</span><span class="n">Environment</span><span class="p">]::</span><span class="n">UserName</span><span class="w">

</span><span class="c"># 4</span><span class="w">
</span><span class="err">$</span><span class="p">(</span><span class="n">Get-WMIObject</span><span class="w"> </span><span class="nt">-class</span><span class="w"> </span><span class="nx">Win32_ComputerSystem</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">select</span><span class="w"> </span><span class="nx">username</span><span class="p">)</span><span class="o">.</span><span class="nf">username</span><span class="w">
</span></code></pre></div></div>

<p>All the above methods are more or less equivalents and will yield the following results:</p>

<ol>
  <li>PsCustomObject  - Which is my samAccountName</li>
  <li>domain\PsCustomObject - Same as above just with the domain prefix</li>
  <li>PsCustomObject - Identical to number 1</li>
  <li>domain\PsCustomObject - Identical to number 2</li>
</ol>

<p>Issue is none of them will easily return the <strong>UserPrincipalName</strong> of the currently logged in user.</p>

<p>This can easily be achieved with the following command:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">([</span><span class="n">ADSI</span><span class="p">]</span><span class="s2">"LDAP://&lt;SID=</span><span class="si">$(</span><span class="p">[</span><span class="n">System.Security.Principal.WindowsIdentity</span><span class="p">]::</span><span class="n">GetCurrent</span><span class="p">(</span><span class="si">)</span><span class="s2">.User.Value)&gt;"</span><span class="p">)</span><span class="o">.</span><span class="nf">UserPrincipalName</span><span class="w">
</span></code></pre></div></div>

<p>This command, despite not as user friendly as the other commands, will return the full UPN of the user for example <strong>PsCustomObject@domain.com</strong>.</p>

<p>I encourage you to explore the other available methods and properties of the <em>[System.Security.Principal.WindowsIdentity]</em> <a href="https://docs.microsoft.com/en-us/dotnet/api/system.security.principal.windowsidentity?view=net-5.0">class</a> as it can be really handy when trying to get details about the current user.</p>]]></content><author><name>SingularVector</name><email>PsCustomObject@outlook.com</email></author><category term="PowerShell" /><category term="Tips" /><category term="HowTo" /><category term="PowerShell" /><category term="Tips" /><summary type="html"><![CDATA[In this post we will explore how to easily retrieve currently logged in user UPN via PowerShell]]></summary></entry></feed>