<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Senator Networks</title>
    <description>The latest articles on DEV Community by Senator Networks (@senatornetworks).</description>
    <link>https://dev.to/senatornetworks</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4196248%2F45a0fe30-8bca-4be4-8c10-98a47dc6c221.png</url>
      <title>DEV Community: Senator Networks</title>
      <link>https://dev.to/senatornetworks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/senatornetworks"/>
    <language>en</language>
    <item>
      <title>A Microsoft 365 Onboarding and Offboarding Runbook for Small-Office Help Desks</title>
      <dc:creator>Senator Networks</dc:creator>
      <pubDate>Wed, 21 Oct 2026 13:49:53 +0000</pubDate>
      <link>https://dev.to/senatornetworks/a-microsoft-365-onboarding-and-offboarding-runbook-for-small-office-help-desks-1o2m</link>
      <guid>https://dev.to/senatornetworks/a-microsoft-365-onboarding-and-offboarding-runbook-for-small-office-help-desks-1o2m</guid>
      <description>&lt;p&gt;Most small-office help desks don't fail on hard problems. They fail on the boring, repeatable ones: a new hire who can't sign in on day one, a leaver whose mailbox is still forwarding to a personal Gmail three months later, a shared laptop nobody can unlock. Every one of those tickets is cheap to prevent and expensive to clean up.&lt;/p&gt;

&lt;p&gt;This post is the onboarding and offboarding runbook we keep for Microsoft 365 tenants with 10 to 150 people, written as scripts instead of a checklist so the steps actually happen in the same order every time. Everything below uses the Microsoft Graph PowerShell SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Connect with the smallest scopes you need
&lt;/h2&gt;

&lt;p&gt;Don't run day-to-day help desk scripts as a Global Administrator. Connect with delegated scopes that match the job, and use a dedicated admin account protected by phishing-resistant MFA.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Install-Module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Microsoft.Graph&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CurrentUser&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Connect-MgGraph&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Scopes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User.ReadWrite.All"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"Group.ReadWrite.All"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"Directory.Read.All"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-MgContext&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Scopes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a script only reads data (reports, audits), connect with &lt;code&gt;User.Read.All&lt;/code&gt; and nothing else. It's one line of discipline that keeps a typo from becoming an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Onboarding: groups do the work, not people
&lt;/h2&gt;

&lt;p&gt;The single biggest improvement for a small help desk is to stop assigning licences, Teams, SharePoint sites and shared mailboxes one by one. Assign them to a handful of role groups once, then onboarding becomes "create user, add to role group".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nx"&gt;AccountEnabled&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nx"&gt;DisplayName&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Priya Shah"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nx"&gt;MailNickname&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pshah"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nx"&gt;UserPrincipalName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pshah@contoso.ca"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nx"&gt;UsageLocation&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CA"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nx"&gt;PasswordProfile&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ForceChangePasswordNextSignIn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;New&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Guid&lt;/span&gt;&lt;span class="err"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;Guid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-MgUser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Role group carries the licence (group-based licensing) and app access&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-MgGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"displayName eq 'Role - Accounting'"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-MgGroupMember&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-GroupId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$group&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DirectoryObjectId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details that save tickets later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;UsageLocation&lt;/code&gt; before licensing.&lt;/strong&gt; Licence assignment fails silently in group-based licensing if it's missing, and the new hire shows up on Monday with no mailbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send the temporary password out of band&lt;/strong&gt; (a phone call or the manager's hands), never to the personal email the person used to apply.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Pre-stage the device, not just the account
&lt;/h2&gt;

&lt;p&gt;An account without a ready laptop is still a day-one ticket. If the devices are enrolled in Intune with Windows Autopilot, assign the device to the user before it ships so the first sign-in pulls policy, apps and OneDrive Known Folder Move on its own. If you're not on Autopilot yet, at least keep a written image checklist: BitLocker on, local admin removed, updates current, the line-of-business apps installed and tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Offboarding: order matters
&lt;/h2&gt;

&lt;p&gt;Offboarding is where small businesses leak data. The order below closes access first, preserves data second, and cleans up last.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$upn&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"former.employee@contoso.ca"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-MgUser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-UserId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$upn&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 1. Block sign-in and kill existing sessions&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Update-MgUser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-UserId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AccountEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Revoke-MgUserSignInSession&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-UserId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 2. Record group memberships before removing them (you will be asked later)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-MgUserMemberOf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-UserId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-All&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Name'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AdditionalProperties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Export-Csv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".\&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MailNickname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;-groups.csv"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoTypeInformation&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 3. Remove from groups (licences drop with the role group)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-MgUserMemberOf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-UserId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-All&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Remove-MgGroupMemberByRef&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-GroupId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DirectoryObjectId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in Exchange Online PowerShell, convert the mailbox to shared (so it stays searchable without a licence, within the size limit) and check for forwarding rules the person set themselves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Connect-ExchangeOnline&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;Set-Mailbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$upn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Shared&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-InboxRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Mailbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$upn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForwardTo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RedirectTo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForwardAsAttachmentTo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Set-Mailbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$upn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForwardingSmtpAddress&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DeliverToMailboxAndForward&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That forwarding check is the step people skip, and it's the one that matters most when someone leaves on bad terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Make the help desk the system of record
&lt;/h2&gt;

&lt;p&gt;Scripts only help if someone can see what happened. Log every run, who ran it and for which user, to a place the whole team can read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Start-Transcript&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"\\fileserver\it-logs\offboard-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MailNickname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;yyyyMMdd-HHmm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.txt"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# ... runbook steps ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Stop-Transcript&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attach the transcript to the ticket. Six months later, when an auditor or a lawyer asks "when exactly did this person lose access?", the answer is one search away instead of a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. What to measure
&lt;/h2&gt;

&lt;p&gt;For a small help desk, three numbers tell you most of what you need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Day-one success rate&lt;/strong&gt;: new hires who could sign in, open email and reach their files on their first morning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to revoke&lt;/strong&gt;: minutes from "HR says they're gone" to sign-in blocked and sessions revoked. Aim for under 15.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeat tickets&lt;/strong&gt;: the same issue from the same person within 30 days. A high number means you're closing tickets, not fixing problems.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;None of this needs expensive tooling. It needs the steps written down once, run the same way every time, and logged. That's the approach our &lt;a href="https://www.senatornetworks.com/services/toronto/managed-it/help-desk" rel="noopener noreferrer"&gt;IT help desk Toronto&lt;/a&gt; engineers use for small and mid-sized offices, and the scripts above are a good starting point for any team running Microsoft 365, wherever you are.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work at Senator Networks, a managed IT provider. The scripts are generic Microsoft Graph and Exchange Online PowerShell and work in any tenant; test them against a non-production user first.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>sysadmin</category>
      <category>microsoft</category>
      <category>security</category>
    </item>
    <item>
      <title>Keeping Canadian Data in Canada: A Practical Setup for Small-Business Web Apps</title>
      <dc:creator>Senator Networks</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:32:14 +0000</pubDate>
      <link>https://dev.to/senatornetworks/keeping-canadian-data-in-canada-a-practical-setup-for-small-business-web-apps-3imo</link>
      <guid>https://dev.to/senatornetworks/keeping-canadian-data-in-canada-a-practical-setup-for-small-business-web-apps-3imo</guid>
      <description>&lt;p&gt;Most of the custom apps we scope for Canadian small and mid-sized businesses start the same way: someone asks for a client portal, an intake form or an internal dashboard, and halfway through the first meeting the question comes up: "Does our data have to stay in Canada?"&lt;/p&gt;

&lt;p&gt;The honest answer is "it depends", but the engineering answer is that keeping it in Canada is cheap to do on day one and painful to do later. This post is the checklist we use to pin a small web app to Canadian infrastructure from the start, with the config snippets that make it stick.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Know what the rules actually say
&lt;/h2&gt;

&lt;p&gt;Before touching Terraform, get the requirement straight, because "data residency" gets used loosely.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PIPEDA&lt;/strong&gt; (federal private-sector privacy law) does not ban storing personal data outside Canada. It makes you accountable for it, and expects you to tell people when their data may be processed in another country.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ontario's PHIPA&lt;/strong&gt; applies to personal health information. Health custodians and their agents usually end up requiring Canadian storage in their contracts, even where the statute doesn't strictly demand it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Federally regulated financial institutions&lt;/strong&gt; follow OSFI guidelines on outsourcing and technology risk, and their vendor questionnaires will ask exactly where data and backups live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public-sector clients&lt;/strong&gt; (municipalities, school boards, some provincial agencies) frequently write Canadian residency into the RFP itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the real driver is usually a contract or a customer's procurement team, not the law alone. Ask for the clause in writing, because it tells you whether backups, logs and support access are covered too.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pick regions that are actually in Canada
&lt;/h2&gt;

&lt;p&gt;All three big clouds have Canadian regions, and two of them have a region in or near Toronto:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;Canada Central&lt;/td&gt;
&lt;td&gt;Toronto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;Canada East&lt;/td&gt;
&lt;td&gt;Quebec City&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;ca-central-1&lt;/td&gt;
&lt;td&gt;Montreal area&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;ca-west-1&lt;/td&gt;
&lt;td&gt;Calgary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud&lt;/td&gt;
&lt;td&gt;northamerica-northeast2&lt;/td&gt;
&lt;td&gt;Toronto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud&lt;/td&gt;
&lt;td&gt;northamerica-northeast1&lt;/td&gt;
&lt;td&gt;Montreal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a Toronto business, Azure Canada Central or GCP northamerica-northeast2 keeps latency low for staff and customers, and a second Canadian region gives you somewhere to put backups without leaving the country.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pin the region in code, not in a wiki
&lt;/h2&gt;

&lt;p&gt;The most common way data leaks out of Canada is not a breach. It's a developer creating one "temporary" resource in the default region. Make the allowed regions a hard rule.&lt;/p&gt;

&lt;p&gt;On Azure, an &lt;code&gt;allowedLocations&lt;/code&gt; policy assignment at the subscription level does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"if"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"not"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"canadacentral"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"canadaeast"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"global"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"then"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On AWS, a Service Control Policy with an &lt;code&gt;aws:RequestedRegion&lt;/code&gt; condition does the same job. In Terraform, fail fast in the module itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"location"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"canadacentral"&lt;/span&gt;

  &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;"canadacentral"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"canadaeast"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"This app must be deployed to a Canadian region."&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The validation catches mistakes in review. The cloud policy catches anything created by hand in the portal.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Check the services that quietly go global
&lt;/h2&gt;

&lt;p&gt;Pinning your compute and database is the easy part. These are the pieces that tend to slip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backups and geo-replication.&lt;/strong&gt; Azure's paired region for Canada Central is Canada East, so geo-redundant storage stays in Canada. Double-check the pairing for any other provider or region you use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email and SMS providers.&lt;/strong&gt; Transactional email services often process and store message bodies in the US. If your portal emails documents, link to them instead of attaching them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error tracking and logging SaaS.&lt;/strong&gt; A stack trace can contain a name, an email or a health card number. Scrub request bodies before they leave, or pick a vendor with a Canadian or self-hosted option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDNs.&lt;/strong&gt; Static assets cached worldwide are fine. Authenticated API responses should be marked &lt;code&gt;Cache-Control: private, no-store&lt;/code&gt; so the edge never stores them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI features.&lt;/strong&gt; If you add summarisation or a chatbot, check which region the model endpoint runs in. Several managed AI services are available in Canadian regions now, but the default is often a US one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A small Express middleware is enough to stop personal data ending up in a CDN cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;private, no-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Encrypt, and decide who holds the keys
&lt;/h2&gt;

&lt;p&gt;Encryption at rest is on by default for the managed databases on all three clouds. The stronger answer for a regulated client is a customer-managed key in a Canadian Key Vault or KMS instance, so the key itself never leaves the region either. It adds a little operational work (key rotation, access policies) but it's the line in the security questionnaire that ends the conversation fastest.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Write it down for the customer
&lt;/h2&gt;

&lt;p&gt;Last step, and the one teams skip: a one-page data map. List each component, the region it runs in, where its backups go, and which third parties can see the data. When the next procurement questionnaire arrives, you answer it in ten minutes instead of a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;None of this is exotic. Region validation, a deny policy, a review of the "quietly global" services and a data map cover what most Canadian SMB apps need. Doing it before the first deploy costs an afternoon. Doing it after launch means migrating databases.&lt;/p&gt;

&lt;p&gt;We build and support apps like these for businesses across the GTA; if that's useful context, here's our page on &lt;a href="https://www.senatornetworks.com/services/toronto/app-dev" rel="noopener noreferrer"&gt;app development Toronto&lt;/a&gt; projects. Questions or corrections on any of the above are welcome in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: this post is published by Senator Networks, a Toronto IT services and app development company.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cloud</category>
      <category>privacy</category>
      <category>azure</category>
    </item>
  </channel>
</rss>
