EDFI-2776: Windows/IIS installation scripts - #234
Conversation
Automates the official Windows install docs to produce a working AdminApp at https://localhost/adminapp. Scripts live in windows-install folder. Pipeline: - setup-vm-prereqs.ps1: scan-then-install for IIS / SQL Server / Git (optional on already-prepared machines) - install-all.ps1: master orchestrator with phased re-run flags (-OnlyPhase1, -SkipPhase1, -SkipPhase2, -SkipPreflightCheck, etc.)
Adds -DbEngine (mssql default, pgsql alternative) so the same installer targets either engine, plus -UsePostgresDocker for a dockerized Postgres in local installs. - install-all.ps1 / 04-deploy-api.ps1 / 00-check-prereqs.ps1 / uninstall.ps1: engine-aware preflight, production.js patching, [user] probe, admin-user upsert, summary, and teardown - New windows-install/docker/ assets: docker-compose.yml (postgres:16-alpine + cert-init + SSL + healthcheck), .env.example, init/01-create-adminapp-user.sh - Idempotent re-run: re-applies the app user's password + grants via psql on every bring-up (init scripts only run on fresh volumes) - Drive-by: 03a-prereqs-runtime.ps1 routes `java -version` through `cmd /c ... 2>&1` to dodge PS 5.1 NativeCommandError wrapping
Three modes selected by install-all.ps1: disabled (default, USE_YOPASS=false), -YopassUrl (existing Yopass), or -SetupYopassDocker (stand up locally via docker-compose, pinned to jhaals/yopass:12.5.0 on container port 80). Preflight checks that Docker is running and the publish port is free, allowing our own container for idempotent re-runs. Also fixes a pre-existing wiring gap where -YopassUrl was declared on install-all.ps1 but never forwarded to 04-deploy-api.ps1.
Rename the per-section install scripts so their numbering follows the official Windows IIS installation guide, and pull the Keycloak and Yopass helpers out of the numbered sequence. Only file names and the references to them change; no script logic or behavior is altered. 01-prereqs-sql.ps1 -> 02-prereqs-sql.ps1 02-prereqs-iis.ps1 -> 01-prereqs-iis.ps1 03b-keycloak-start.ps1 -> idp-keycloak-start.ps1 03c-build-project.ps1 -> 04-build.ps1 03d-yopass-docker.ps1 -> yopass-docker.ps1 04-deploy-api.ps1 -> 05-deploy-api.ps1 05-deploy-fe.ps1 -> 06-deploy-fe.ps1
Split the old runtime prereq script and fold the Node helper and Keycloak
bootstrap into two focused scripts, matching the generic vs IdP-example
separation:
03a-prereqs-runtime.ps1 + 00a-fix-node.ps1 -> 03-prereqs-node.ps1
(Node install, nvm remediation, npm cache; no Java, no Keycloak)
03a (JDK + Keycloak download) + 06-keycloak-bootstrap.ps1
-> idp-keycloak-setup.ps1
(JDK, download, start via idp-keycloak-start.ps1, then provision
realm/client/user inline; one run leaves Keycloak ready)
idp-keycloak-start.ps1 now points users to idp-keycloak-setup.ps1 when
kc.bat is missing. install-all.ps1 is rewired: Node runs once up front
(idempotent), and all Keycloak work happens in Phase 3 through
idp-keycloak-setup.ps1.
00-check-prereqs.ps1 and README.md still reference the old script names in
advisory text; those are reconciled in later phases.
Move the local Keycloak IdP teardown out of the generic uninstaller so the
two concerns are independent:
uninstall-keycloak.ps1 (new)
- stop the Keycloak process (kc.bat/keycloak/quarkus java + :8080)
- delete the Keycloak install dir (honors -KeepKeycloakDownload)
- unset Machine JAVA_HOME
uninstall.ps1 (generic only)
- drop the Keycloak stop step, the install-dir delete, and JAVA_HOME
from the env-var unset; remove the -KeepKeycloakDownload param
- renumber the remaining teardown sections
- add an informational final step that detects Keycloak leftovers
(install dir, JAVA_HOME, a running Keycloak process) and suggests
running uninstall-keycloak.ps1, without acting on them
-KeycloakInstallPath stays on uninstall.ps1 for the leftover check only.
Replace the single nested "Ed-Fi" IIS site (API + FE under one site, which
broke base-path routing) with two independent standalone HTTP sites:
EdFi-AdminApp-API http://localhost:3333
EdFi-AdminApp-FE http://localhost:4200
No self-signed cert and no 443 binding; local dev is HTTP only.
01-prereqs-iis.ps1 drop cert generation, Trusted Root add, 443 binding,
and the Ed-Fi parent-site creation; keep URL Rewrite,
iisnode, the handlers unlock, and the
HTTP_X_ORIGINAL_URL allow
04-build.ps1 VITE_API_URL=http://localhost:3333, VITE_BASE_PATH="/"
05-deploy-api.ps1 standalone site is the only mode; MY_URL/FE_URL default
to the :3333 / :4200 sites; drop the nested branch and
the unused ParentSiteName/AppAlias/ApiPort/FePort params
06-deploy-fe.ps1 standalone site is the only mode; drop the nested branch
idp-keycloak-setup.ps1 client redirect/origin URIs default to the new
standalone HTTP URLs
install-all.ps1 smoke test hits http://localhost:3333 (no cert trust
needed); summary URLs and notes updated for HTTP
uninstall.ps1 is aligned with the new topology in a follow-up commit.
The install now creates two standalone HTTP sites instead of a nested
Ed-Fi site with a self-signed cert, so the teardown is simplified to match:
- Remove the standalone sites EdFi-AdminApp-API and EdFi-AdminApp-FE
(the API was previously a sub-application under the Ed-Fi site).
- Drop the now-dead parent-site removal, the 0.0.0.0:443 SSL binding
removal, and the self-signed cert removal section.
- Remove the -RemoveParentEdFiSite, -KeepCert, -CertFriendlyName, and
-ParentSiteName params.
- Always delete the two deployed subdirs (and the parent dir if empty);
no more surgical-vs-full-tree branch.
- Renumber the remaining teardown sections.
The global iisnode-all handler scrub, database/Yopass teardown, npm cache,
NPM_CONFIG_CACHE unset, and the informational Keycloak-leftovers check are
unchanged.
The Admin App's auth engine is provider-agnostic (generic OIDC discovery);
the Keycloak coupling was only in the install scripts' parameter names. Make
the API deploy provider-neutral so any OIDC IdP (Entra, Google, Auth0, ...)
works by overriding the defaults.
05-deploy-api.ps1
- rename -Keycloak* params to -Oidc* (Issuer, ClientId, ClientSecret,
ManagementDomain, MachineSecret); Keycloak values kept as example
defaults
- add -OidcScope (default 'openid email profile') and patch production.js
scope: '' -> scope: 'openid email profile'
install-all.ps1 pass the client secret as -OidcClientSecret
04-build.ps1 frame VITE_IDP_ACCOUNT_URL as the IdP account URL (the
Keycloak value is just the example default)
install-all.ps1 keeps its Keycloak-named params: it is the local-Keycloak
example orchestrator (it runs idp-keycloak-setup).
Update 00-check-prereqs.ps1 (deferred from the Phase 1 restructure) so its
advisory text and collision checks match the new script names and the
standalone HTTP topology:
- point every "<script> will ..." hint at the renamed scripts
(03-prereqs-node, idp-keycloak-setup, idp-keycloak-start, 02/04/05/06)
- a too-old Node is now INFO, not FAIL: 03-prereqs-node remediates it via
nvm-windows
- attribute the JDK install to idp-keycloak-setup (the optional IdP path)
- replace the obsolete Ed-Fi-site / HTTPS:443 collision checks with a
standalone-site view: check the EdFi-AdminApp-API site, and flag ports
3333 (API) and 4200 (FE) already in use by a foreign process
install-all.ps1: update the -AcceptRisks help and the risk prompt to mention
ports 3333/4200 instead of the removed :443 binding.
Scripts relied on implicit machine-state handoffs; add fail-early guards with
actionable messages so a missing prerequisite is obvious instead of surfacing
as a cryptic error later.
01-prereqs-iis.ps1 require the IIS role (W3SVC) before adding URL Rewrite +
iisnode
02-prereqs-sql.ps1 require sqlcmd on PATH (used throughout)
05-deploy-api.ps1 guard the WebAdministration import with an actionable
message; require NPM_CONFIG_CACHE (set by
03-prereqs-node) so npm under iisnode has a writable cache
06-deploy-fe.ps1 guard the WebAdministration import
idp-keycloak-start.ps1 require a JDK (java on PATH or a valid JAVA_HOME)
before starting Keycloak; point to idp-keycloak-setup
04-build.ps1 already guards Node on PATH and package.json; 03-prereqs-node and
idp-keycloak-setup are installers with no upstream precondition.
The npm cache override was set as a Machine env var, which changed npm's cache
location for every user and process on the box (env vars outrank user/project
.npmrc). Scope it to the API App Pool instead so the user's other npm usage is
untouched, and remove the implicit 03 -> 05 handoff.
03-prereqs-node.ps1 drop the npm cache folder + Machine NPM_CONFIG_CACHE and
the -NpmCachePath param; it is now Node-only
05-deploy-api.ps1 own the cache: create the cache folder, grant the App
Pool Modify, and set NPM_CONFIG_CACHE in the App Pool's
environmentVariables; add -NpmCachePath; drop the
NPM_CONFIG_CACHE precondition (no longer a handoff)
00-check-prereqs.ps1 check the cache folder (pool-scoped now) and drop the
obsolete machine NPM_CONFIG_CACHE collision risk
uninstall.ps1 the pool env var goes away with the App Pool; stop
unsetting a Machine NPM_CONFIG_CACHE we no longer set
The App Pool environmentVariables collection requires IIS 10+ and takes effect
on the next worker start; to be confirmed in the Phase 6 end-to-end run.
Wrap the failure-prone operations in try/catch that re-throw with context
(likely cause + fix + the original error) instead of surfacing a raw
exception. Every catch ends in throw -- nothing is swallowed.
01-prereqs-iis.ps1 network errors on the URL Rewrite / iisnode downloads
02-prereqs-sql.ps1 SQL registry writes (Mixed Mode + TCP/IP) and the
service restart
03-prereqs-node.ps1 the nodejs.org AV-fallback download
04-build.ps1 seeding packages\fe\.env
05-deploy-api.ps1 robocopy copies (clearer messages), App Pool create/
config, IIS site create/update, web.config and
production.js writes, and the App Pool NPM_CONFIG_CACHE
env var (notes the IIS 10+ requirement)
Rewrite the README for the restructured scripts and the standalone HTTP
topology:
- end state is HTTP (FE http://localhost:4200, API http://localhost:3333);
no self-signed cert or 443 binding
- group the scripts into generic path / local IdP example / transversal,
with new names and an official-guide section mapping
- add an "Other identity providers" section (generic OIDC via -Oidc* on
05-deploy-api; Keycloak is just the local example)
- split the uninstall docs (uninstall.ps1 + uninstall-keycloak.ps1)
- refresh the install-all phase walk-through, flags, and known issues
(npm cache scoped to the App Pool, Node via 03-prereqs-node, HTTP-only
TLS note)
The App Pool environmentVariables collection that 05-deploy-api uses to scope
NPM_CONFIG_CACHE was added in IIS 10.0, so fail early when IIS is older instead
of erroring during deploy.
01-prereqs-iis.ps1 throw if HKLM:\...\InetStp MajorVersion < 10
00-check-prereqs.ps1 report IIS version (FAIL on < 10) so the pre-flight
aborts before install-all starts
The standalone API/FE sites deployed into C:\inetpub\Ed-Fi\adminapp-api and
\adminapp -- subfolders of an existing Ed-Fi site's root on machines that
already host one (e.g. alongside AdminApi). IIS then nested them visually under
Ed-Fi and that site re-exposed the content over :443, undermining the
"independent sites" goal. Deploy to dedicated top-level directories instead.
05-deploy-api.ps1 DestPath default -> C:\inetpub\EdFi-AdminApp-API
06-deploy-fe.ps1 DestPath default -> C:\inetpub\EdFi-AdminApp-FE
uninstall.ps1 delete those dedicated dirs (ApiDestPath/FeDestPath
replace InetpubPath; drop the empty-parent cleanup)
install-all.ps1 iisnode log path in the failure hint
README.md rate-limit production.js path
Found during the local end-to-end test on a machine sharing the Ed-Fi site
with AdminApi.
00-check-prereqs.ps1 is the pre-flight for the generic Admin App install; Keycloak is the optional local-IdP example, so its checks don't belong here (and shouldn't fire when 00-check is run on its own or with a non-Keycloak provider). Drop them: the Java (JDK) diagnostic + RISK, the Keycloak download and running/realm checks, the -KeycloakInstallPath param, and the $MinJavaMajor constant. 00-check is now provider-agnostic. Nothing is lost: idp-keycloak-setup.ps1 already installs/detects the JDK and idp-keycloak-start.ps1 already requires one. The "your Java/JAVA_HOME will be modified" heads-up moves into idp-keycloak-setup.ps1, where the mutation actually happens.
install-all is now provider-agnostic via a mandatory -IdpProvider
(keycloak | microsoft | google | other), instead of being hardwired to the
local Keycloak example.
- keycloak: unchanged -- runs idp-keycloak-setup, requires
-KeycloakAdminPassword/-TestUserPassword.
- microsoft | google | other: skip the local IdP setup; require
-OidcIssuer/-OidcClientId/-OidcClientSecret (issuer/account-url defaulted
per provider); validate the issuer discovery endpoint; print the redirect/
origin URIs to register; seed the [user] row for -AdminUsername (the
identity itself lives in the external IdP).
- rename -KeycloakClientSecret -> -OidcClientSecret (the OIDC client secret
in every mode); pass -Oidc*/-ViteIdpAccountUrl through to 05-deploy-api and
04-build; mode-aware install summary.
README: document -IdpProvider, the external-provider flow, and where to find
the OIDC issuer.
-OidcManagementDomain / -OidcMachineSecret patched production.js's MANAGEMENT_DOMAIN / MANAGEMENT_CLIENT_SECRET for an IdP management (M2M) client that the install scripts never provision and the deployed AdminApp doesn't need for the validated flows (login, environments; Ed-Fi API client credentials live in the Admin DB, not the IdP). Remove the params and their production.js patches; the template keeps its inert MANAGEMENT_* defaults. Re-add deliberately (provider-specific) if that feature is ever wired up.
Correct doc-comment inaccuracies in the windows-install scripts so they match the actual behavior (and the installation guide): - install-all.ps1: 01-prereqs-iis sets up URL Rewrite + iisnode and unlocks the handlers section (HTTP only); it does not create an HTTPS cert or binding. - install-all.ps1: the npm cache is configured by 05-deploy-api (App-Pool-scoped), not by 03-prereqs-node. - idp-keycloak-setup.ps1: FeBaseUrl/ApiBaseUrl default to the standalone HTTP sites (FE 4200, API 3333), not the sub-app deployment. - yopass-docker.ps1: the published host port maps to container port 80, not 1337. - docker-compose.yopass.yml and 05-deploy-api.ps1: drop references to the old script numbering (03d, 03a) that no longer exists after the renumber. No functional changes.
b39a637 to
a61aa92
Compare
idp-keycloak-start.ps1 redirected Keycloak's output to pipes it never read; a full pipe buffer can block startup so it never becomes ready. Launch via Start-Process with output redirected to log files instead. No change to the readiness check or detached-process behavior.
Architecture Review: Windows Installation Scripts vs. PRD FT3Overall AssessmentThese scripts are a competent, well-engineered automation of a Windows/IIS Admin App install. The headline architectural point is that the Node hosting approach both diverges from FT3 and rests on an abandoned dependency — and, helpfully, a single change resolves both at once. FT3 prescribes three things (
So the design-of-record (PRD) and the as-built scripts disagree, and the as-built path also depends on iisnode, which is effectively EOL. The recommendation below (a The engineering quality of what was built is high: idempotency is taken seriously, error messages are actionable, the pre-flight check is genuinely useful, and field edge cases (AV quarantine of Node, Keycloak first-run bootstrap, PG reserved-word quoting) are handled with evident experience. The Node Hosting Question: iisnode vs. httpPlatform vs. service + ARRRecommendation: replace iisnode with a How the current scripts wire it ( Why a Satisfies FT3. IIS launches Architecturally better than iisnode. iisnode is abandoned (last release v0.2.26, 2020; no security fixes) and hardcodes the Two concrete options, in preference order:
Migration cost is small: Both use the same <handlers>
<add name="httpPlatformHandler" path="*" verb="*"
modules="httpPlatformHandler" resourceType="Unspecified" />
</handlers>
<httpPlatform processPath="C:\Program Files\nodejs\node.exe"
arguments="main.js"
stdoutLogEnabled="true" stdoutLogFile=".\logs\node-stdout.log"
startupTimeLimit="60" startupRetryCount="3">
<environmentVariables>
<environmentVariable name="NODE_ENV" value="production" />
</environmentVariables>
</httpPlatform>Why not the standalone Windows Service + ARR reverse proxy (the literal FT3 design): it is the heaviest operational burden for the least Node-experienced persona — ARR is fiddly to configure as a reverse proxy, and the service wrapper is its own rabbit hole ( Key Architectural Strengths
Concerns, Ranked by Impact1. (High) Node hosting: iisnode diverges from FT3 and depends on an EOL module. Covered above. Resolution: adopt a 2. (High) Docker is mixed into a "Windows-native / IIS" install, against the persona. The PRD persona has little Docker experience ( 3. (High) HTTP-only with no TLS path is the only supported end state. No HTTPS binding, cert, or 443 ( 4. (Medium) Config applied by brittle literal string replacement. 5. (Medium) Keycloak runs via 6. (Low) Environment assumptions. Default SQL instance only ( 7. (Low) Secrets written in plaintext to SummaryHigh-quality automation with strong idempotency, modularity, pre-flight, and teardown. The headline finding: the Node hosting approach both diverges from FT3's IIS-reverse-proxy design and depends on the abandoned iisnode module — and a This is an AI-assisted review based on reading (not executing) the scripts. Please verify before acting, particularly the HttpBridge maintenance/RC status and the iisnode/nvm node-path coupling. 🤖 Generated with Claude Code |
There was a problem hiding this comment.
I wonder about the necessity of these Compose files, when we already have a nice set of files in the root compose/ directory. We can refactor those files if the code here offers improvements.
I am not 100% opposed to these additional files, but I do want to make sure we have thought through the implications and that we're ready to maintain some files that are near duplicates.
| if (Test-Path $iisnodeDll) { | ||
| Write-Host "iisnode already installed at $iisnodeDll" | ||
| } else { | ||
| $url = "https://github.com/Azure/iisnode/releases/download/$IisNodeVersion/iisnode-full-$IisNodeVersion-x64.msi" |
There was a problem hiding this comment.
Suggestion: also put the file hash process here.
iisnode has not had a release in many years, so it would be fine to hard-code the specific version here.
Functionality Review: FT3 Windows Server InstallationSummary of the implemented architectureThe scripts deploy a different topology than FT3 prescribes. Instead of the Node backend as a Windows service on port 8135 fronted by IIS as a reverse-proxy, the scripts host the Node API inside IIS via iisnode as a standalone HTTP site on port 3333 (
1. Requirement-by-requirement coverage
2. Functional gaps / bugs that could block end-to-end install
3. Is the condition of satisfaction plausibly met?Yes for the local Keycloak path; qualified yes for the Entra/external path. A reader running This is an AI-assisted review based on reading (not executing) the scripts. Please verify before acting — particularly the seed-migration/ 🤖 Generated with Claude Code |
Security Review: Windows Install ScriptsScope: all scripts in CriticalC1. Hardcoded application data-encryption key shipped in the deployed config and never rotated
HighH1. Stale management client secret left at its hardcoded default
H2. Secrets passed on the command line and persisted in plaintext install summary
H3. PostgreSQL superuser password passed via
|
The app builds each OIDC callback as /api/auth/callback/<id> from the oidc row's auto-generated id (oidc.strategy.ts), but the scripts hardcoded /1, assuming the seeded row always gets id=1. That only holds on a clean install; if a prior oidc row existed, the new id != 1 and the registered redirect URI no longer matches what the app sends, so login fails (PR #234 Functionality review, Gap B). After the app boots, read back the real id of the row the installer manages (for its clientId) and use it for the redirect URI. The id is a DB-row property, so this runs for every provider: - Keycloak (script-provisioned client): idp-keycloak-setup gains a -RedirectCallbackId param (default 1); install-all re-invokes it with the real id only when it differs, correcting the client's redirectUris idempotently against the already-running Keycloak. - Entra/Google/other (user-provisioned): no script can touch the provider's portal, so install-all prints the exact redirect URI to register. No literal /callback/1 remains in install-all.ps1 or idp-keycloak-setup.ps1. The README example is deferred to the Tier 4 docs pass (tracked under T4.4).
Node, Keycloak, and JDK downloads ran without integrity checks (PR #234 Security review M4). Node zips are now verified against nodejs.org's official SHASUMS256.txt for the resolved version; the Keycloak zip against a pinned SHA-256 (default) or -KeycloakSha256; and a -JdkDownloadUrl zip against a required -JdkSha256 over HTTPS only. Already-downloaded files are reused when their hash matches.
Add baseline security headers to the API and FE web.config: HSTS, Referrer-Policy, and a Content-Security-Policy in Report-Only mode (enforcing lands with the always-on TLS work). Remove the X-Powered-By header from both sites. Node already emits X-Content-Type-Options and X-Frame-Options, so those are added only on the FE to avoid duplicate headers on the API's proxied responses. Give 06-deploy-fe.ps1 an -ApiUrl parameter so the CSP connect-src names the exact API origin the FE bundle calls. Default the API's IIS error mode to DetailedLocalOnly with a -DevErrors opt-in, so stack detail is not exposed to remote clients by default.
06-deploy-fe.ps1 created the FE site without an application pool, so it bound to DefaultAppPool. When that pool is Stopped (common after a reboot or recycle) the SPA returned 503. Create a dedicated EdFi-AdminApp-FE pool, assign the site to it, and start it explicitly (the pool auto-starts on reboot). uninstall.ps1 tears the pool down, guarded so it never removes DefaultAppPool.
The client registered a wildcard redirect and post-logout URI (/*), far broader than the app actually uses. The app's only OIDC redirect_uri is the API callback (auth.controller.ts / oidc.strategy.ts) and its only post_logout_redirect_uri is MY_URL/api/auth/post-logout, from which it forwards to the FE itself. Drop the wildcard from both redirectUris and post.logout.redirect.uris, keeping the explicit paths. Force a client rewrite when a wildcard from an earlier install survives, so a re-run strips it. Login and logout verified end to end.
Add an HTTPS binding to the API (3443) and FE (4443) IIS sites, always on. The certificate is resolved by precedence: -CertificateThumbprint, an imported -CertificatePfxPath, or a self-signed cert auto-generated for localhost (keeping the local quick-start working). 05-deploy-api.ps1 resolves the cert and 06-deploy-fe.ps1 reuses it (matched by FriendlyName), so both sites share one certificate. install-all.ps1 forwards the HTTPS ports and cert parameters to both. The HTTP bindings stay in place; the HTTP->HTTPS redirect and https URL propagation follow in later Tier 3 steps.
Add a 301 redirect rule to the API and FE web.config so plain-HTTP requests are permanently redirected to the HTTPS port (3443/4443). The rule captures the host without its port and targets the explicit HTTPS port. On the API it is a new <rewrite> block that runs before the httpPlatform handler, so the redirect fires without proxying to node; on the FE it precedes the SPA fallback rule. 301 (Permanent) is used because IIS URL Rewrite has no 308; it pairs with the HSTS header and the POST->GET downgrade is moot once app URLs are all https.
Derive the API and FE URLs as https on the mirror ports in install-all.ps1 and pass -ViteApiUrl to 04-build.ps1 (previously not forwarded, so the bundle baked the http default). Since Vite bakes VITE_API_URL at build time, this is what lets the FE target HTTPS. The API/FE production config and Keycloak client still carry http here; T3.4 flips those so the full flow is https.
Flip the app URLs to https so the whole flow runs over TLS: install-all passes https ApiUrl/FeUrl to 05 (production.js MY_URL/FE_URL/WHITELISTED_REDIRECTS via NODE_CONFIG), the https API origin to 06 (CSP connect-src), and https ApiBaseUrl/FeBaseUrl to the Keycloak client so its redirect/origin URIs match. The standalone defaults in 05, 06 and idp-keycloak-setup move to https on the mirror ports, the printed redirect/summary URLs use the resolved https URLs, and the smoke test hits https (accepting the self-signed cert for the local check). Keycloak's own issuer stays http (TLS for Keycloak is a later item).
… ports uninstall.ps1 now removes the HTTP.sys SSL certificate bindings for the API and FE HTTPS ports (which persist after Remove-Website) and deletes the auto-generated self-signed certificate, matched by FriendlyName so a user-supplied cert is left untouched. 00-check-prereqs.ps1 adds pre-flight port checks for the HTTPS ports (3443/4443) alongside the existing HTTP ports.
The smoke test now shells out to curl.exe instead of Invoke-WebRequest: PS 5.1's
Invoke-WebRequest cannot complete the TLS handshake to the self-signed HTTPS
binding ("unexpected error on send"), even with a cert-validation bypass and
TLS 1.2 forced, whereas curl.exe (bundled since Win10 1803 / Server 2019) does.
The retry window is widened to ~3 minutes because a fresh cold start runs
migrations and the catalog sync before the first request is served, and the
post-boot steps are gated on the smoke test. The external-provider OIDC discovery
check now enables TLS 1.2 on ServicePointManager so the handshake to Entra/Google
(which require TLS 1.2+) does not fail under PS 5.1's default protocol set.
Flip the CSP header on the API and FE from Content-Security-Policy-Report-Only to enforcing Content-Security-Policy now that TLS is always-on. Validated in a browser against the deployed sites with no violations: the FE policy (default-src 'self', MUI's inline styles allowed, connect-src limited to the API origin) and the API policy (default-src 'none' -- safe because the API serves only JSON in production; Swagger UI is disabled when NODE_ENV=production). The deploy success messages in 05 and 06 now point at the HTTPS URLs (the HTTP ports only redirect).
…efault When the deploy generates a self-signed cert (no real cert supplied), add its public part to LocalMachine\Root so browsers on the host trust it and do not show a "Not Secure" warning -- the common local/quick-start case. Only the self-signed path does this; a supplied real cert is already CA-trusted and is never added to Root. A -SkipSelfSignedTrust switch (threaded through install-all to 05/06) opts out where policy forbids adding trusted roots. The trusted copy carries the same FriendlyName, and uninstall now removes the cert from both My and Root, matching only our FriendlyName so unrelated localhost roots are never touched.
Keycloak omits false boolean and empty array client properties from the client JSON, so the direct property reads in the client-exists-update comparison throw under Set-StrictMode -Version Latest. Read optional properties through a Get-KcClientProp guard so the diff stays StrictMode-safe; normal-path behavior is unchanged.
The API disabled SSL verification for its outbound HTTPS calls (ODS/API, AdminApi, Yopass), so it accepted any certificate and was exposed to man-in-the-middle attacks off-box. Ship SSL_VERIFICATION on by default in the production config template and via NODE_CONFIG, with a -DisableSslVerification opt-out (and a loud warning) for local/dev upstreams that use self-signed certificates Node's CA store won't trust.
Direct Access Grants (the OAuth password/ROPC grant) sends user credentials straight to the token endpoint and is a local-testing convenience only. Warn when it is enabled against a non-loopback Keycloak, and note the same in the parameter documentation.
The compose file published the Postgres port on 0.0.0.0, so on a multi-homed host the database was reachable over the network, gated only by the password. Bind to 127.0.0.1 by default via a new POSTGRES_BIND_HOST variable; set it to 0.0.0.0 to opt into remote exposure (documented, and only over TLS).
The example Keycloak IdP was launched with Start-Process, so it died on reboot and had to be restarted by hand. Add an opt-in -RegisterKeycloakStartupTask (install-all) / -RegisterStartupTask (idp-keycloak scripts) that registers a startup Scheduled Task to relaunch it as SYSTEM at boot; tear the task down in uninstall-keycloak.ps1 via schtasks (robust against the Get-ScheduledTask CIM enumeration error a single corrupt task on the machine can cause). Also warn on every launch that start-dev is local-dev-only and document kc.bat start with a hostname, real database, and TLS for anything beyond local dev.
SSL_VERIFICATION is on by default, so adding an Environment against a self-signed or dev ODS/API or Admin API fails with a certificate error and the fix was only discoverable in a parameter comment. Add a README note covering the -DisableSslVerification opt-out and the NODE_EXTRA_CA_CERTS / --use-system-ca alternatives, and print the same heads-up at the end of install-all when verification is left on.
… and security state
…user The Admin App creates the citext extension and the pg-boss job-queue schema at boot, both of which require CREATE on the database. init/01 and the existing-volume sync granted only CONNECT plus public-schema ownership, so the API failed to start with "permission denied for database sbaa". Grant CONNECT and CREATE (citext is a trusted extension, so no superuser is needed), keeping the user a non-superuser with no database-wide GRANT ALL.
…g PID The uninstall killed the API's node child by extracting its PID from the stdout-log filename, assuming Microsoft HttpPlatformHandler's node-stdout.log_<pid>_<timestamp>.log naming. LeXtudio HttpBridge names those logs differently, so the first digit group is a 14-digit timestamp; casting it to [int] overflowed Int32 and threw. Match the node process by the deployment directory in its command line instead -- handler-agnostic and never throws.
…talled one Both HttpBridge and Microsoft HttpPlatformHandler register the same 'httpPlatformHandler' module, so the re-run-safe skip could not tell them apart and silently kept whichever was installed -- a -HttpHandler choice was ignored without notice. Detect the installed handler MSI and warn, with the manual uninstall steps, when it differs from the requested -HttpHandler.
…timestamp The build-current heuristic checked only that artifacts exist and are newer than package.json, so a dist built for a different VITE_API_URL was reused and deployed. Under the enforcing CSP that stale bundle calls a disallowed origin and the app fails after login. Skip the build only when the last-built .env matches the requested VITE_API_URL, and clear the nx cache (which ignores .env) when the FE config changed so the bundle is genuinely rebuilt.
…install The best-effort [oidc] reconciliation already fell back to a warning on a non-zero sqlcmd exit, but the try had no catch: under $ErrorActionPreference = 'Stop' a -b native error is a terminating NativeCommandError, so a transient SQL login timeout aborted the whole install before the exit code was checked. On a first install this step is a guaranteed no-op (the [oidc] table does not exist yet). Catch the error and fall through to the warning, and raise the sqlcmd login timeout to 30s for headroom under load.
|
Closing this PR — the windows-install suite has moved to its own repository The entire windows-install/ suite has been imported into New PR: Ed-Fi-Exchange-OSS/Admin-App-Installation-Scripts#2 (branch EDFI-2776_Windows_Install_Scripts). All the work reviewed here is carried forward: the final commit in the new repo already incorporates every fix from this PR's review cycle. Why the full review history stays here: this PR retains the complete discussion and the commit-level history of how each finding was diagnosed and fixed. The new repo starts from a clean import commit, so please refer back to this PR for that context. E2E validation (against Admin App v4.0.1): the full matrix passed (documented in the new PR's description) — SQL Server and PostgreSQL/Docker; Keycloak, Entra ID, and Google as OIDC providers, including live browser logins. Entra ID OIDC seeding was validated on both engines (closes EDFI-2774). Review continues in the new repository. |
Draft. Opening early for visibility and feedback on structure and approach. See "What is still open" before a full review pass.
Relates to:
Summary
Adds a set of PowerShell scripts under
/windows-installthat automate the Windows/IIS installation of Admin App v4. The scripts are numbered to match the order of the official installation guide, and can be run end to end viainstall-all.ps1or one at a time. The flow is provider agnostic: it works with any OIDC provider, with an optional local Keycloak example kept separate from the generic path.What is included
00to06covering prerequisite checks, IIS/SQL/Node prerequisites, build and deployment of the API and frontend.install-all.ps1to run the full sequence in one pass.uninstall.ps1aligned with the standalone topology.idp-keycloak-setup.ps1andidp-keycloak-start.ps1, with teardown inuninstall-keycloak.ps1, kept separate from the generic install path.-DbEngine.What is still open
Testing / QA performed
install-all.ps1on a clean Windows Server/IIS target00through06in guide order-DbEngine)