Skip to content

Configuration

GitHub Action edited this page Oct 21, 2026 · 15 revisions

Configuration

Important

The kit's first-class deliverable is the SharePoint farm configuration in scripts/sps/. The PDC, PULL and SQL .psd1 files documented here are reference samples only — in production you should describe those tiers in your own tooling and only keep Secrets.psd1 + CfgAppSps.psd1 from this kit.

SPSConfigKit is driven by two PowerShell data files:

  1. scripts/Secrets.psd1 — centralised credential material.
  2. scripts/sps/CfgAppSps.psd1 — the SharePoint farm ConfigurationData consumed by CfgAppSps.ps1.

The configuration scripts (CfgAppSps.ps1, plus the example PDC / PULL / SQL scripts) import both files and compile one MOF per node defined in AllNodes.

File layout

scripts/
├── Secrets.sample.psd1         ← tracked credentials template (placeholders)
├── Secrets.psd1                ← your real credentials (git-ignored; copy of the sample)
├── init/
│   ├── Initialize-DscNode.psd1            ← prerequisite manifest (modules, certs)
│   ├── Initialize-DscNode.ps1
│   ├── Initialize-DscEncryption.ps1
│   ├── Initialize-SoftwarePackages.psd1   ← package manifest (binaries, LPs, CUs)
│   └── Initialize-SoftwarePackages.ps1
├── sps/
│   ├── CfgAppSps.psd1          ← SharePoint farm ConfigurationData
│   └── CfgAppSps.ps1
├── pdc/                        ← REFERENCE EXAMPLE — not for production
├── pull/                       ← REFERENCE EXAMPLE — not for production
└── sql/                        ← REFERENCE EXAMPLE — not for production

Secrets.psd1

A single serviceAccounts array. Each entry has a stable Name (used as the variable name when the loader materialises a PSCredential) and an optional IsAdAccount flag:

Key Required Description
Name yes Stable identifier, used as the PSCredential variable name in the configuration scripts.
DisplayName yes Human-readable label, surfaced in logs.
Description yes Free-form description.
Username yes DOMAIN\sam for AD accounts; any non-empty value for non-AD containers (the loader requires it).
Password yes Plain-text password. Replace with your own secret-management hook before checking the file in.
IsAdAccount no Default $True. Set to $False for containers that aren't real AD accounts (PFX passwords, DSRM, passphrase).

Example:

@{
    serviceAccounts = @(
        @{  # AD account — IsAdAccount omitted, defaults to $True
            Name        = 'SETUP'
            DisplayName = 'SharePoint SETUP'
            Description = 'SharePoint Setup Account'
            Username    = 'CONTOSO\svcspssetup'
            Password    = '************'
        }
        @{  # Non-AD container — used as a PFX password
            Name        = 'SharePointCert'
            DisplayName = 'SharePoint cert PFX'
            Description = 'PFX password for the SharePoint web-app certificate'
            Username    = 'SharePointCert'
            IsAdAccount = $False
            Password    = '************'
        }
    )
}

Caution

Never commit real passwords to source control. Only the tracked Secrets.sample.psd1 template ships (with '******************' placeholders); the real Secrets.psd1 you fill in is git-ignored, so your credentials never reach the repository. Copy the sample to Secrets.psd1 and replace the placeholders with a wrapper that pulls from your secret store (Azure Key Vault, CyberArk, etc.) before the file is read in any environment.

Which brick each account belongs to

Important

The bundled Secrets.psd1 is an example that lists EVERY account for EVERY brick of the kit — including bricks you do not deploy. Declaring an account here does not mean it is used: for the SharePoint brick, only the accounts listed in the SharePoint.ManagedAccounts allowlist are actually registered on the farm. Accounts that belong to a brick you are not deploying (for example the PULL server or the lab AD/PDC) can simply be left untouched or removed.

The table below maps every account in the sample Secrets.psd1 to the brick that consumes it, so you can tell at a glance whether a given account is relevant to your deployment.

Account Brick (config) Optional?
ADSETUP, ADSAFEMODE AD / PDC (CfgAppPdc) Yes — lab / demo domain only
SQLSERVER, SQLServerCert SQL (CfgAppSql) No (standard deployment)
PULLSETUP, IISPULLAPP, DscPullCert PULL server (CfgAppPull) Yes — only when a node declares IsPullServer
SETUP, FARM, IISAPP, SEARCH, CONTENT, SUPERUSER, SUPEREADER, Passphrase, SharePointCert, OfficeOnlineCert SharePoint (CfgAppSps) No — core farm accounts
ADSYNC User Profile AD Import (CfgAppSps; lab grant in CfgAppPdc) Yes — only when User Profile AD Import is used
SWM SharePoint Workflow Manager (SWM server) Yes — only when an SWM server is deployed
  • The AD / PDC brick provisions an Active Directory domain controller. It is meant for labs and demos; in production the domain already exists, so these accounts are not used.
  • The PULL server brick is only compiled when at least one node is flagged IsPullServer. If you deploy in push mode, PULLSETUP, IISPULLAPP and DscPullCert are never consumed.
  • The SharePoint brick is the core of the kit. SETUP is the SharePoint installation account; the remaining accounts are the farm / service / app-pool identities actually used to build the farm.

Note

Even within the SharePoint brick, the accounts registered as SPManagedAccount resources are filtered by the SharePoint.ManagedAccounts allowlist (default @('FARM', 'IISAPP', 'SEARCH')). Anything outside that allowlist — including the PULL / SQL / OOS accounts above — is intentionally ignored and never reaches the SharePoint MOF.

User Profile AD Import sync account (ADSYNC)

The User Profile Service Application runs in AD Import mode (NoILMUsed = $true). AD Import reads user objects directly from Active Directory using a dedicated sync account (ADSYNC, e.g. CONTOSO\svcspsync). That account must hold the Replicate Directory Changes permission on the domain it synchronises with.

  • Production: the account and its Replicate Directory Changes grant are provided by your AD team. Add the account to Secrets.psd1 so the farm can use its credential — the kit changes nothing in your directory.
  • Lab / demo (CfgAppPdc): when an ADSYNC entry is present, CfgAppPdc creates the account and grants it Replicate Directory Changes (the DS-Replication-Get-Changes extended right) on the domain root via the ADObjectPermissionEntry resource. This runs only on the lab domain controller (inside the IsADSServer block) and is skipped entirely when no ADSYNC entry exists.

The import itself is driven by an optional synchronization connection declared under NonNodeData.SharePoint.Services.UserProfile.SyncConnection in CfgAppSps.psd1:

SyncConnection = @{
  Forest      = 'contoso.com'                   # AD forest to import from
  SyncAccount = 'ADSYNC'                         # Secrets account -> import credential
  IncludedOUs = @('OU=INT,DC=contoso,DC=com')   # OUs holding the users to import
}

CfgAppSps turns this into a native SPUserProfileSyncConnection (an Active Directory Import connection) attached to the User Profile Service. SyncAccount names the Secrets.psd1 account whose credential is used to read the directory (ADSYNC above). Remove the whole SyncConnection block to skip the connection — nothing is provisioned then. No SPUserProfileSyncService is used: that belongs to the legacy FIM/MIM synchronization, whereas this kit runs User Profiles in AD Import mode (NoILMUsed = $true).

Note

The kit targets SharePoint Subscription Edition. On SPSE the SPUserProfileSyncConnection resource forces the connection name to the forest with dots replaced by dashes (e.g. contoso-com) and does not reconcile an excluded-OUs list, so the kit does not expose a Name or ExcludedOUs setting — the connection is named from the forest automatically, and the import scope is defined by IncludedOUs.

My Sites (personal sites)

Once users are imported (AD Import above), you can provision the My Site infrastructure from an optional NonNodeData.SharePoint.Services.MySite block in CfgAppSps.psd1:

MySite = @{
  WebAppUrl          = 'https://sharepoint.contoso.com'  # web app hosting the My Site host
  QuotaTemplateName  = 'MySite'                           # quota template to create/apply
  DatabaseNamePrefix = 'DSPS_CONTENT_MySite_'             # personal-site content DB prefix
  UserCount          = 500                                # expected number of personal sites
  QuotaMaxMB         = 2048                               # per-site storage limit
  QuotaWarningMB     = 1843                               # per-site warning threshold
  MaxDBSizeGB        = 100                                # cap per content database
  # Optional overrides (skip the auto-calculation):
  # NumberOfDatabases = 10
  # MaximumSiteCount  = 50
  # WarningSiteCount  = 45
}

CfgAppSps turns this into an SPQuotaTemplate plus a set of dedicated personal-site SPContentDatabase resources. Their number is sized from the expected user base so no database exceeds the Microsoft-supported content-database size:

MaxDBSizeMB       = MaxDBSizeGB * 1024
MaximumSiteCount  = floor(MaxDBSizeMB / QuotaMaxMB)   # sites per DB (capped at 10000)
WarningSiteCount  = floor(MaximumSiteCount * 0.9)
NumberOfDatabases = max( ceil(UserCount * QuotaMaxMB / MaxDBSizeMB),   # storage-based
                         ceil(UserCount / MaximumSiteCount) )          # site-capacity-based

Example: 500 users × 2048 MB ÷ (100 × 1024) MB = 10 databases, each holding up to 50 personal sites. The site-capacity term matters once MaximumSiteCount is floored or capped (e.g. very large quotas, or the 10 000-site cap), so the kit takes the larger of the two counts. SharePoint load-balances new personal sites across these databases automatically. Set NumberOfDatabases, MaximumSiteCount or WarningSiteCount explicitly to override the calculation. Remove the whole MySite block to skip My Site provisioning — nothing is emitted then. These dedicated personal-site databases (DSPS_CONTENT_MySite_01, _02, …) are separate from and additional to the My Site host content database.

The companion scripts/sps/Invoke-MySiteProvisioning.ps1 script pre-creates the personal sites after the service is configured: it enumerates the User Profile Service profiles, skips the service accounts listed in Secrets.psd1, calls CreatePersonalSite() for each remaining user, and applies the quota template. Run it from an elevated SharePoint Management Shell (which exposes the SharePoint cmdlets — on Subscription Edition they are not loadable via Import-Module or Add-PSSnapin), as the SharePoint farm account (the "System Account"). The farm account is required because creating personal sites on behalf of other users goes through self-service site creation, which only the farm account may perform — any other account (even a User Profile Service Application administrator) is denied in SelfServiceCreateSite. The script verifies this and stops if run as any other account or from a non-elevated shell. Use -WhatIf first to preview the sites that would be created.

Tip

In the User Profile Service Application, under Setup My Sites → My Site Cleanup, set a Secondary Owner (e.g. the SharePoint FARM account). When a user's profile is deleted, their My Site is retained for 30 days and access is granted to the manager or, failing that, this secondary owner, so content can be recovered before the site is removed. This is optional and independent of provisioning, but recommended for governance.

Note

Microsoft-supported limits used by the sizing model: 200 GB per content database and up to 10 000 personal sites per database. The default MaxDBSizeGB = 100 stays comfortably under the 200 GB limit; the kit warns if you set it higher and caps MaximumSiteCount at 10 000.

Note

Personal sites are created under the personal wildcard managed path (e.g. https://sharepoint.contoso.com/personal/<user>). You do not need to declare this path in the web application's ManagedPath list: SharePoint provisions it automatically when the User Profile Service Application is created with a My Site host location (SPUserProfileServiceApp + MySiteHostLocation, which the kit already configures). You can confirm it exists with Get-SPManagedPath -WebApplication <url> (look for personal / WildcardInclusion).

How the loader uses IsAdAccount

  • IsAdAccount -ne $false is the filter that selects AD accounts. Because $null -ne $false is $true, omitting the key includes the entry, which is the desired default for AD accounts.
  • Containers (ADSETUP, ADSAFEMODE, DscPullCert, SharePointCert, OfficeOnlineCert, SQLServerCert, ...) must set IsAdAccount = $False to be excluded from $spManagedAccounts and similar AD-only collections.

Certificate ↔ Secrets naming

Certificate Name values in CfgAppSps.psd1 are intentionally chosen to match Secrets entries 1:1 (with a Cert suffix to make the intent obvious). This lets the configuration scripts resolve each cert's PFX password directly:

CertificatePassword = (Get-Variable -Name $spCertificate.Name -ValueOnly)

A missing or misnamed Secrets entry fails fast at compile time.

scripts/sps/CfgAppSps.psd1

The file is a single hashtable with two top-level keys: AllNodes and NonNodeData.

AllNodes

Per-node settings. The first entry must be the wildcard '*' node that declares the DSC plain-text/domain-user policy:

AllNodes = @(
    @{
        NodeName                    = '*'
        PSDscAllowPlainTextPassword = $True
        PSDscAllowDomainUser        = $True
    }
    @{
        NodeName     = 'APP1'
        IsMaster     = $true
        IsSPSServer  = $True
        SPVersion    = 'SE'
        SPServerRole = 'Application'  # ApplicationWithSearch | DistributedCache | Search | …
        LocalAdmins  = @('CONTOSO\svcspssetup', 'CONTOSO\svcspsfarm')
    }
    # WFE1, SCH1, OOS1 …
)

Node-selector flags:

Flag Used by
IsSPSServer All SharePoint Node blocks.
IsMaster Designates the farm-creator (vs. join-farm) node.
IsOOSServer, IsOOSSingle Office Online Server Node block.
SPServerRole SPFarm.ServerRole (MinRole).
IsSrcAdmin, IsSrcCrawl, IsCntProc, IsSrcAnalyt, IsSrcQuery, IsIndexPart Search topology component placement.
LocalAdmins Group BUILTIN\Administrators membership.

Note

The non-master node selector uses -not $_.IsMaster, so you can omit IsMaster on non-master nodes — missing key and explicit $false both work.

Search topology placement (SPServerRole and Is* flags)

The Search Service Application topology places its six components (Admin, Crawler, ContentProcessing, AnalyticsProcessing, QueryProcessing, IndexPartition) on the nodes whose Is* flag is set and whose SPServerRole is either Search (a dedicated search node) or ApplicationWithSearch (a combined Application + Search node). Set the relevant Is* flags to $true on whichever node hosts each component.

On a small 2-server farm you can drop the dedicated Search node and host every search component on the Application master by giving it the ApplicationWithSearch MinRole plus the six flags:

@{
    NodeName     = 'APP1'
    IsMaster     = $true
    IsSPSServer  = $True
    SPVersion    = 'SE'
    SPServerRole = 'ApplicationWithSearch'
    IsSrcAdmin   = $True
    IsSrcCrawl   = $True
    IsCntProc    = $True
    IsSrcAnalyt  = $True
    IsSrcQuery   = $True
    IsIndexPart  = $True
    LocalAdmins  = @('CONTOSO\svcspssetup', 'CONTOSO\svcspsfarm', 'CONTOSO\svcspsearch')
}
@{
    NodeName     = 'WFE1'
    IsAFCache    = $true
    IsSPSServer  = $True
    SPVersion    = 'SE'
    SPServerRole = 'WebFrontEndWithDistributedCache'
    LocalAdmins  = @('CONTOSO\svcspssetup', 'CONTOSO\svcspsfarm')
}

Note

A commented 2-server example ships alongside the default 3-node search layout in scripts/sps/CfgAppSps.psd1.

NonNodeData

Flat container of farm-wide settings:

NonNodeData = @{
    # The share host is defined ONCE here. Host it on a MEMBER server (e.g. the
    # pull server), never on a domain controller (a node already holds a machine
    # session to the DC, and Windows refuses a second identity to the same server,
    # so a share on the DC fails at apply with "Access is denied").
    SourcePath = '\\PULL\Softwarepackages'    # binaries + .cer/.pfx share
    DomainName = 'contoso.com'

    # Set to $false when the customer manages storage themselves. Default $true.
    ManageDisks = $true
    # Data disks, initialised by scripts/init/Initialize-DscDisks.ps1 (keyed by
    # disk Number). This is the single source of truth for storage: the Drives.{Data,Logs,Temp}
    # hashtable consumed everywhere else is DERIVED from it by Type (see below).
    # Disk numbers are environment-specific — a plain VM is usually 0=OS, 1=Data,
    # 2=Logs; an Azure VM WITH a temp disk shifts to 0=OS, 1=<temp>, 2=Data,
    # 3=Logs. Adjust Id to match 'Get-Disk' on the node.
    Disks      = @(
        @{ Id = '0'; Letter = 'C'; Type = 'OS'  ; FSLabel = 'SYSTEM'; AllocationUnitSize = 4KB }
        @{ Id = '2'; Letter = 'F'; Type = 'Data'; FSLabel = 'DATA'  ; AllocationUnitSize = 4KB }
        @{ Id = '3'; Letter = 'G'; Type = 'Logs'; FSLabel = 'LOGS'  ; AllocationUnitSize = 4KB }
    )

    ADC        = @{
        # Each entry carries only the .cer / .pfx FILE NAME; the full path is
        # derived by the Cfg*.ps1 script as SourcePath + CerFileName / PfxFileName.
        # (An explicit CertPath / PfxPath is still honoured for backward compat.)
        certificates = @(
            @{ Name = 'SharePointCert'    ; FriendlyName = 'SharePoint'  ; CerFileName = 'SharePoint.cer'  ; PfxFileName = 'SharePoint.pfx'  }
            @{ Name = 'OfficeOnlineCert'  ; FriendlyName = 'OOSCertSSL'  ; CerFileName = 'OfficeOnline.cer'; PfxFileName = 'OfficeOnline.pfx' }
            @{ Name = 'SQLServerCert'     ; FriendlyName = 'SQLCertSSL'  ; CerFileName = 'SQLServer.cer'   ; PfxFileName = 'SQLServer.pfx'    }
            @{ Name = 'DscPullCert'       ; FriendlyName = 'DSCPull'     ; CerFileName = 'DscPull.cer'     ; PfxFileName = 'DscPull.pfx'      }
        )
    }

    SQLAlias   = @(
        @{ Name = 'ADMIN'   ; ServerAlias = 'SINGLE-ADM-SPSSQL' ; ServerName = 'SQL1' ; InstanceName = 'MSSQLSERVER' ; Port = 1433 }
        @{ Name = 'SEARCH'  ; … }
        @{ Name = 'SERVICES'; … }
        @{ Name = 'CONTENT' ; … }
    )

    IIS        = @{ LogFolder = 'LOGS\IIS' ; httpErrFolder = 'LOGS\HTTPERR' }
    OOS        = @{ AllServers = @('OOS') ; URL = 'oosweb.contoso.com' ; … }
    SharePoint = @{
        ProductKey                = '…'
        FarmConfigDatabaseName    = 'DSPS_Admin_Config'
        CentralAdministrationPort = '443'
        CentralAdministrationUrl  = 'https://sharepoint-admin.contoso.com'
        ServiceAppPoolName        = 'SharePointServiceApplications'
        WebApplications           = @(
            @{
                Name            = 'SharePoint'
                Url             = 'https://sharepoint.contoso.com'
                HostHeader      = 'sharepoint.contoso.com'
                Port            = 443
                CertName        = 'SharePointCert'  # ← indirection into ADC.certificates / Secrets
                ContentDBName   = 'DSPS_CONTENT_MySite'
                ApplicationPool = 'PAMYSITE'
                Path            = 'C:\InetPub\…\SharePoint'
                ManagedPath     = @( @{ Name = 'Search' ; Explicit = $true ; RelativeUrl = 'search' } )
                Sites           = @(
                    @{ Name = 'MySiteHost'   ; Url = 'https://sharepoint.contoso.com'              ; Template = 'SPSMSITEHOST#0' ; Language = 1033 ; ContentDatabase = 'DSPS_CONTENT_MySite' }
                    @{ Name = 'SearchCenter' ; Url = 'https://sharepoint.contoso.com/search'       ; Template = 'SRCHCEN#0'      ; Language = 1033 ; ContentDatabase = 'DSPS_CONTENT_SearchCenter' }
                    @{ Name = 'AppsCatalog'  ; Url = 'https://sharepoint.contoso.com/sites/apps'   ; Template = 'APPCATALOG#0'   ; Language = 1033 ; ContentDatabase = 'DSPS_CONTENT_AppsCatalog' }
                )
            }
        )
        Services                  = @{
            SearchService               = @{ Name = 'SVCSearchService' ; DatabaseName = 'DSPS_SCH_SearchService' ; SearchCenterUrl = 'https://sharepoint.contoso.com/search' ; ContentSources = @{ … } }
            UserProfile                 = @{ … }
            ManagedMetadataService      = @{ … }
            AppManagementService        = @{ … }
            SubscriptionSettingsService = @{ … }
            StateService                = @{ … }
            SessionState                = @{ … }
            UsageService                = @{ … }
        }
    }
}

The full reference file lives at scripts/sps/CfgAppSps.psd1 and is the canonical schema — treat the snippet above as a map.

Key conventions

  • Cert names end in Cert and match Secrets entries 1:1.

  • WebApplications carry a CertName field so a friendly Name can be used in MOF identifiers while the cert lookup still resolves through Secrets.

  • SourcePath is the SMB share that hosts every binary, language pack, CU, .cer, and .pfx referenced by the kit. Configure share-level read for the setup account on every node.

  • Disks is the storage source of truth — an ordered array of physical data disks the node should own. Each entry declares:

    Key Required Description
    Id yes The disk Number as reported by Get-Disk. Portable across bare-metal, VMware, Hyper-V and Azure — not an Azure LUN. Numbers are environment-specific: a plain VM is usually 0=OS, 1=Data, 2=Logs, but an Azure VM with a temporary disk shifts to 0=OS, 1=<temp>, 2=Data, 3=Logs. Adjust to match the target node.
    Letter yes Drive letter to assign (C, F, G, …). Also the key used to derive Drives.
    Type yes Semantic role: OS, Data, Logs, or Temp. Drives the derivation and the OS-exclusion filter.
    FSLabel yes NTFS volume label (UPPERCASE convention: SYSTEM / DATA / LOGS / TEMP).
    AllocationUnitSize yes Cluster size (e.g. 4KB; SQL data/log volumes use 64KB).

    Declaring at least three disks (SYSTEM / DATA / LOGS) is the recommended baseline for every tier.

  • Drives is DERIVED, never hand-written — each Cfg*.ps1 builds NonNodeData.Drives = @{ Data; Logs; Temp } from Disks (keyed by Type) right after loading the psd1, so the ~30 existing Drives.Data / Drives.Logs consumers keep working unchanged. Temp falls back to the Data letter when no dedicated Temp disk is declared. A drive letter is therefore declared once (in Disks) instead of duplicated. Drives.Data / Drives.Logs prefix every per-node path so each farm role can point at different physical drives without editing the script.

  • ManageDisks (bool, default $true) gates disk initialisation by the scripts/init/Initialize-DscDisks.ps1 bootstrap script. Disk preparation is a one-time node-prep step (like joining the domain or installing the DSC modules), so it runs during bootstrap — not inside the recurring application MOF — and reads this same Disks block via -ConfigPath. When $true, the script onlines each raw non-OS disk, applies a GPT partition, formats NTFS with the requested FSLabel / AllocationUnitSize and assigns the drive letter, so a brand-new farm has its volumes before anything writes to them. The OS disk is never touched. Set ManageDisks = $false when the customer has already initialised and formatted their volumes: the script exits without touching any disk, but Drives is still derived by the configuration so every path resolves. The script is idempotent and non-destructive — a disk that already carries data is reported and left intact, never reformatted.

  • Per-product path overrides (optional) — if your customer's file hierarchy doesn't match the kit's default (<SourcePath>\SPS → <Drives.Data>\SoftwarePackages\SPS\{BIN,LP,CU} and the same shape for OOS), override any subset of the following keys in NonNodeData.SharePoint and / or NonNodeData.OOS — defaults preserve the original layout:

    Key Purpose Default
    SourcePath Network share holding the product's setup files. <NonNodeData.SourcePath>\<SPS|OOS>
    DestinationPath Local folder the setup files are copied to. <Drives.Data>\SoftwarePackages\<SPS|OOS>
    Subfolders.Binaries Sub-folder of DestinationPath holding the main installer (setup.exe / prereq). BIN
    Subfolders.LanguagePack Sub-folder of DestinationPath holding per-locale Language Pack installers. LP
    Subfolders.CumulativeUpdate Sub-folder of DestinationPath holding the CU package(s). CU

    Existing absolute values for SharePoint.UberCumulativeUpdate and OOS.CUFileName keep working unchanged: any rooted path is used as-is, otherwise the value is resolved under <DestinationPath>\<Subfolders.CumulativeUpdate>.

  • SharePoint.LanguagePacks (optional) — array of SharePoint locale codes (e.g. @('fr-fr','es-es')). Each entry must match a sub-folder under <DestinationPath>\<Subfolders.LanguagePack>\ that contains the corresponding setup.exe. Omit or leave as @() to skip Language Pack installation entirely.

  • SharePoint.CentralAdministrationUrl (optional) — a vanity URL for Central Administration. When set to an HTTPS URL (e.g. https://sharepoint-admin.contoso.com), Central Admin is served over SSL on that host instead of http://<node>:<port>. This requires: a certificate named SharePointAdminCert in ADC.certificates whose SAN covers the host (issued by CfgAppPdc), a matching Secrets.psd1 entry (PFX password), and a DNS record for the host pointing at the Central Admin node (published by CfgAppPdc). Set CentralAdministrationPort to match the scheme (443 for HTTPS). Leave the URL empty/absent to keep Central Admin on plain HTTP at the port.

    On SharePoint Subscription Edition the certificate is bound through SharePoint Certificate Management (never a manual IIS binding), in three coordinated steps:

    1. SPFarm provisions the Central Admin HTTPS binding and host header (it does not assign a certificate, so Central Admin is not yet reachable over HTTPS).
    2. The SPCertificate loop imports SharePointAdminCert into the SharePoint certificate store (EndEntity) after the farm exists.
    3. A Script resource binds that managed certificate to the Central Admin Default zone (Get-SPCertificate -Store EndEntity → Set-SPWebApplication -Certificate -UseServerNameIndication), which mirrors how SPWebApplicationExtension binds certificates on SPSE and is what makes Central Admin serve HTTPS.

    [!NOTE] The SharePoint SE certificate binding for Central Administration should be validated on a real farm. Verify with Get-SPWebApplication -IncludeCentralAdministration | Format-List DisplayName, Url and confirm the IIS SharePoint Central Administration v4 site has an https binding on the expected port with the SharePointAdmin certificate and SNI enabled.

  • SharePoint.ManagedAccounts (optional) — allowlist of Secrets.psd1 account names that SharePoint should register as SPManagedAccount resources on the farm master. Defaults to @('FARM', 'IISAPP', 'SEARCH'). Extend the list when you introduce a new dedicated SharePoint service account; leave it untouched to keep the historical set. Anything not in the allowlist (PULLSETUP, IISPULLAPP, SQL / OOS / monitoring accounts …) is intentionally ignored and stays out of the SPS MOF.

  • SharePoint.EdgeAuthAllowlist (optional, off by default) — when present with Enabled = $true, the kit writes the Microsoft Edge Integrated Windows Auth policy HKLM\SOFTWARE\Policies\Microsoft\Edge\AuthServerAllowlist on every SharePoint node. This is a browser policy, normally owned centrally by GPO / Intune — omit the block to let your domain policy stay authoritative (the resource is simply not emitted). Optional Hosts is a list of allowlist patterns; when omitted it defaults to *<DomainName>* (every host in the domain). The value is written with Force, so it overwrites an existing (e.g. GPO-set) value rather than failing.

    EdgeAuthAllowlist = @{
        Enabled = $true
        Hosts   = @('*.contoso.com')   # optional; defaults to *<DomainName>*
    }

    [!TIP] The recommended path is to let the domain manage this policy centrally. CfgAppPdc ships an AuthServerAllowlist entry in NonNodeData.EdgePolicies that the Edge_browser GPO pushes to every domain-joined server (including the SharePoint nodes). When that GPO is in place, leave SharePoint.EdgeAuthAllowlist disabled and let the GPO own the policy.

  • SQL Server path overrides (optional) — scripts/sql/CfgAppSql.psd1 exposes the same SourcePath / DestinationPath keys under NonNodeData.SQL. Defaults are <NonNodeData.SourcePath>\SQL and <Drives.Data>\SoftwarePackages\SQL. SQL doesn't use Subfolders — setup.exe is expected at the root of DestinationPath.

Reference example .psd1s (PDC / PULL / SQL)

scripts/pdc/CfgAppPdc.psd1, scripts/pull/CfgAppPull.psd1, and scripts/sql/CfgAppSql.psd1 follow the same AllNodes + NonNodeData pattern and are meant to be read alongside their .ps1 counterparts.

Use them to see how the loader, the certificate import pattern, and the manifest-driven approach work end-to-end — then replace them with your organisation's production AD, pull-server, and SQL provisioning.

scripts/init/Initialize-SoftwarePackages.psd1

The manifest read by Initialize-SoftwarePackages.ps1. It describes the local file-share repository that backs \\PULL\SoftwarePackages (or whichever SMB share every node downloads binaries from), and the list of packages the script should fetch into it.

@{
    # Root folder of the local SoftwarePackages repository. Every entry
    # below uses a Path RELATIVE to this root.
    Repository = 'F:\SoftwarePackages'

    SoftwarePackages = @(
        @{
            Name        = 'SharePointServerSE'
            Description = 'SharePoint Server Subscription Edition'
            FileName    = 'OfficeServer.iso'
            Url         = 'https://download.microsoft.com/.../OfficeServer.iso'
            Extract     = $true       # mount and copy ISO contents
            Path        = 'SPS\BIN'   # relative to Repository
        }
        @{
            Name        = 'KB5002773'
            Description = 'SharePoint Server SE Cumulative Update'
            FileName    = 'uber-subscription-kb5002773-fullfile-x64-glb.exe'
            Url         = 'https://download.microsoft.com/.../uber-subscription-kb5002773-fullfile-x64-glb.exe'
            Extract     = $false      # direct download to Path
            Path        = 'SPS\CU'
        }
        # …SQL Server 2022 + CU, Language Packs, .NET 4.8, VC++ 2015-2019,
        #   Office Online Server CU…
    )
}

Schema

Key Required Description
Repository yes Local root folder backing the SMB share. Every package Path is resolved against it.
Name yes Identifier used in log output.
Description yes Human-readable label, surfaced in the per-package banner.
FileName yes Source file name; for Extract = $true packages, this is the temp-file name in %TEMP% before mounting.
Url yes Download URL (HTTPS).
Extract yes $true to mount the ISO with Mount-DiskImage and copy its contents into Path; $false to download the file directly to Path.
Path yes Folder under Repository where the file or extracted contents land (e.g. SPS\BIN, SQL\CU, SPS\LP\FR-fr).
Marker no Sentinel file used to short-circuit re-extraction. Defaults to setup.exe, which is correct for the SQL Server, SharePoint, and LP ISOs.

Idempotency

  • For Extract = $true entries, the script skips the package when <Path>\<Marker> already exists.
  • For Extract = $false entries, the script skips the package when <Path>\<FileName> already exists.
  • Downloaded ISOs cached in %TEMP% are reused when a previous run was interrupted between download and extraction.

Why no 7-Zip?

ISO expansion uses Windows' built-in Mount-DiskImage / Copy-Item -Recurse / Dismount-DiskImage pipeline, so the file-share host has zero external dependencies beyond Windows itself. Required capability: Windows 8 / Server 2012 or newer (for Mount-DiskImage).

Note

The script also detects outbound internet access once at startup and skips the download phase entirely on offline hosts. Per-package failures are caught and logged so one bad URL does not abort the whole run.

Validating your ConfigurationData

Before compiling any MOF, run the bundled Pester v5 pre-flight suite against your .psd1 files. It catches the mistakes that would otherwise surface mid-run on the customer site — placeholder product key, duplicate NodeName, missing Secrets.psd1 entry for a managed account, mismatched CertName ↔ ADC.certificates, malformed Language Pack locale, unreachable \\share\setup.exe, missing .pfx …

# Full validation (workstation must have the install share mounted).
.\scripts\test\Invoke-ConfigDataTest.ps1 -ConfigPath .\scripts\sps\CfgAppSps.psd1

# Structure-only (skip every Test-Path on the install share / certificates).
.\scripts\test\Invoke-ConfigDataTest.ps1 -ConfigPath .\scripts\sql\CfgAppSql.psd1 -SkipFilesystem

The driver exits with code 1 on the first failure, so it can be wired into CI or used as a hard gate before Start-DscConfiguration:

.\scripts\test\Invoke-ConfigDataTest.ps1 -ConfigPath .\scripts\sps\CfgAppSps.psd1
if ($LASTEXITCODE -ne 0) { throw 'Config invalid — refusing to compile MOFs.' }

What the suite checks

Every check is read-only. Sections that don't apply to the file under test are skipped automatically (e.g. SharePoint checks are not run against CfgAppSql.psd1).

  • File integrity — Import-PowerShellDataFile succeeds; AllNodes / NonNodeData present; the wildcard * node declares PSDscAllowPlainTextPassword / PSDscAllowDomainUser.
  • AllNodes — NodeName values unique; exactly one IsMaster per role family (SPS, OOS); at least one IsSPSServer / IsOOSServer / IsSQLServer when the matching NonNodeData section is present.
  • Disks & Drives — NonNodeData.Disks is present and each entry has Id / Letter / Type / FSLabel / AllocationUnitSize; disk Ids and Letters are unique; exactly one OS disk; the SYSTEM / DATA / LOGS roles are declared. The Drives.Data / Drives.Logs values derived from Disks match ^[A-Z]:$.
  • SourcePath — NonNodeData.SourcePath is a UNC or rooted local path.
  • Product key — SharePoint.ProductKey matches XXXXX-XXXXX-XXXXX-XXXXX-XXXXX and is not the placeholder XXXXX-....
  • Ports — CentralAdministrationPort and SQLAlias[*].Port are valid integers in 1..65535.
  • Certificates — ADC.certificates[*].Name unique; every cert has a matching Secrets.psd1 entry (drives the PFX password); WebApplications[*].CertName resolves to a known cert Name; OOS.CertFriendlyName resolves to a known FriendlyName.
  • Managed accounts — SharePoint.ManagedAccounts (or the default @('FARM','IISAPP','SEARCH') when omitted) is a subset of Secrets.psd1.
  • Language packs — each SharePoint.LanguagePacks entry matches xx-xx.
  • Filesystem reachability (skipped by -SkipFilesystem) — SourcePath, BIN\setup.exe, LP\<locale>\setup.exe, and the CU package for both SPS and OOS; SQL SourcePath\setup.exe; every ADC.certificates[*].CertPath / PfxPath.

Filesystem checks reuse the exact same Resolve-ProductPaths resolution as CfgAppSps.ps1, so the per-product SourcePath / Subfolders overrides documented above are validated against the same paths the MOF will compile with.

Reading the output

Pester v5 prefixes each It line with a status glyph, and the wrapper adds a one-line summary that also surfaces the number of skipped tests so a clean run never looks misleadingly green:

Glyph Meaning
[+] Passed
[-] Failed — fix before compiling MOFs
[!] Skipped — the section / context didn't apply (e.g. SPS checks against CfgAppSql.psd1, or filesystem checks under -SkipFilesystem). Not a failure.
Tests Passed: 59, Failed: 0, Skipped: 3, Inconclusive: 0, NotRun: 0

59 of 62 test(s) passed; 3 skipped (sections not present in this config
or filesystem checks disabled). ConfigurationData looks healthy.

A run with Failed: 0 is healthy regardless of the skipped count; sections gate themselves off when the relevant NonNodeData sub-tree is missing. The wrapper still returns exit code 0 so CI / pre-MOF gates work the same way.

Prerequisite

The suite requires Pester 5.0.0 or later on the authoring host (Windows PowerShell 5.1 ships with Pester 3.x by default):

Install-Module Pester -MinimumVersion 5.0.0 -Force -SkipPublisherCheck

No other module is needed; the test files use only built-in cmdlets and Pester v5.

Next step

Continue with the Usage page.