EDFI-2776 Add Windows IIS installation scripts - #2
Conversation
Import the windows-install script suite into this dedicated repository as a sibling installation method alongside quick-start/. The scripts automate an end-to-end Windows/IIS install of the Ed-Fi Admin App (v4.0.1 and above) with either SQL Server or a Dockerized PostgreSQL backing store, and Keycloak, Entra ID, or Google Workspace as the OIDC identity provider. Includes the phased installer (install-all plus the 00-06 per-section scripts), Keycloak setup/start, uninstall, the VM prerequisite bootstrap, and the docker/ compose stack for the PostgreSQL and optional Yopass paths. TLS is on by default, the API runs under the httpPlatform handler, and the app connects with a least-privilege database login and a per-install data-encryption key. Validated end-to-end against Admin App v4.0.1 across SQL Server and PostgreSQL with Keycloak, Entra ID, and Google before this import.
Document how to work on the windows-install method: prerequisites, the script layout, install commands per database engine and identity provider, the key install-all parameters, coding conventions, the security invariants to preserve, environment notes, the end-to-end testing expectation, and the pull-request workflow.
After moving the install scripts into this dedicated repo, -SourcePath no longer defaulted to an Admin App checkout. install-all.ps1 now resolves the source: it honors an existing co-located or sibling checkout and otherwise clones Ed-Fi-AdminApp at the newest stable release (-AdminAppRef 'latest', resolved via the GitHub releases API; override to pin a tag). The standalone 00/03/04 scripts resolve the same sibling path; the README, -SourcePath help, and uninstall notes are aligned with the separate-repo layout.
The remove-then-add that keeps NPM_CONFIG_CACHE and NODE_CONFIG idempotent warned on a fresh install, where there is nothing to remove yet; -ErrorAction does not suppress that warning. Add -WarningAction SilentlyContinue so the first-run output stays clean. No behavior change.
There was a problem hiding this comment.
Review - requesting changes
First off: this is a really strong suite - the error-handling discipline, hash-verified downloads, SecureString handling, and docs are well above typical installer scripts, and the E2E matrix shows. That said, I found six issues I'd consider blocking. Four of them are one shared defect class (secret/user values interpolated into SQL/URL/form strings without escaping), so the fixes are all targeted.
| # | Severity | Location | Issue |
|---|---|---|---|
| 1 | High | 05-deploy-api.ps1:423, 06-deploy-fe.ps1:241 |
HSTS on localhost breaks the default Keycloak login |
| 2 | High | 02-prereqs-sql.ps1:213,254, 05-deploy-api.ps1:729 |
Secrets exposed on sqlcmd process command lines |
| 3 | High | install-all.ps1:584 |
Unescaped password in superuser SQL (psql) |
| 4 | High | docker/init/01-create-adminapp-user.sh:19 |
Same unescaped interpolation in the container init SQL |
| 5 | High | idp-keycloak-setup.ps1:412,676 |
Keycloak credentials not URL-encoded in token requests |
| 6 | High | README.md:48,123,143,235 |
Documented commands fail [SecureString] parameter binding |
1. HSTS pin on localhost breaks the default Keycloak flow
05-deploy-api.ps1:423 / 06-deploy-fe.ps1:241
Both sites emit Strict-Transport-Security: max-age=31536000; includeSubDomains, and since the installer auto-trusts its self-signed cert, the browser records a valid one-year HSTS pin for the host localhost. HSTS is port-agnostic, so after the first visit to https://localhost:4443 the browser rewrites the default issuer http://localhost:8080 to https://localhost:8080 - which Keycloak (dev mode) doesn't serve. Login fails with connection refused, and the pin affects every other localhost HTTP service on the machine for a year.
Suggestion: emit HSTS only when a real hostname / CA-issued cert is supplied (skip it on the localhost self-signed path), or serve Keycloak over HTTPS.
2. Secrets on process command lines via sqlcmd -Q
02-prereqs-sql.ps1:213 (sa password), :254 (app-login password), 05-deploy-api.ps1:729 (OIDC client secret)
$saQuery = "ALTER LOGIN sa WITH PASSWORD = '$escapedPw'; ALTER LOGIN sa ENABLE;"
# later: sqlcmd ... -Q $saQueryThe T-SQL containing the plaintext secret becomes a sqlcmd argument - readable by any local user via process listings while it r-line auditing (event 4688 / Sysmon). It also contradicts the suite's own design: 02 uses SQLCMDPASSWORD and promises "never on a command line" (line 82), and the psql branch of the OIDC block already pipes SQL via stdin correctly.
Suggestion: write the statement to an Administrators-only temp file and pass -i (delete in finally), or pipe via stdin lik
3. Unescaped password in superuser SQL
install-all.ps1:584
ALTER USER "$PostgresAppUser" WITH PASSWORD '$PostgresAppPasswordPlain';Runs as the postgres superuser with no quote-escaping. The script's own complexity check encourages symbols, so a password like Summ'er2026! produces a psql syntax error with a misleading "check -PostgresSuperuserPassword" hint - and a crafted value is SQL injection as superuser. Note $OidcClientId is escaped (-replace "'", "''") at ~890, so this is just an omission.
Suggestion: apply the same '' escaping to the password and double any " in the identifiers, or use psql -v variables.
4. Same injection in the Docker first-boot init script
docker/init/01-create-adminapp-user.sh:19
CREATE USER "${ADMIN_APP_DB_USER}" WITH PASSWORD '${ADMIN_APP_DB_PASSWORD}';Unquoted heredoc, so bash expands the .env password straight into SQL. With ON_ERROR_STOP=1, a password containing ' aborts the entrypoint on first boot and the volume never initializes - a bricked container that's hard to diagnose.
Suggestion: psql variables quote safely:
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
-v user="$ADMIN_APP_DB_USER" -v pw="$ADMIN_APP_DB_PASSWORD" <<-'EOSQL'
CREATE USER :"user" WITH PASSWORD :'pw';
GRANT CONNECT, CREATE ON DATABASE :"db" TO :"user";
EOSQL(quoted <<-'EOSQL' so bash stops expanding; pass POSTGRES_DB as -v db= too).
5. Keycloak credentials not URL-encoded in token requests
idp-keycloak-setup.ps1:412 (admin token) and the probe at ~676
-Body "grant_type=password&client_id=admin-cli&username=$AdminUser&password=$AdminPasswordPlain"Any &, =, +, %, or # in the password corrupts the form body invalid_grant. Worse, the recovery guidance then steers keycloak\data`, destroying a working realm over an encoding bug.
Suggestion: pass a hashtable as -Body (Invoke-RestMethod form-encodes it), or wrap each value in [Uri]::EscapeDataString(). Same for client_secret / password in the probe.
6. README quick-start commands don't run as written
README.md:48, :123, :143, ~:235
-SaPassword, -KeycloakAdminPassword, -OidcClientSecret, -TestUserPassword are [SecureString], so passing 'your-sa-password' fails parameter binding immediately - the canonical documented command cannot be executed. It also contradicts CONTRIBUTING.md ("never pass a plaintext literal") and the script's own .EXAMPLE blocks, which do it right.
Suggestion: switch the examples to the script's own style:
.\install-all.ps1 -IdpProvider keycloak `
-SaPassword (Read-Host -AsSecureString 'sa password') `
-KeycloakAdminPassword (Read-Host -AsSecureString 'Keycloak admin password') `
-OidcClientSecret (Read-Host -AsSecureString 'OIDC client secret') `
-TestUserPassword (Read-Host -AsSecureString 'test user password')While in there: the README never mentions -AppDbPassword (required in the default mssql mode per the script's own help) and has ppPassword/-PostgresSuperuserPassword`) - worth adding to fully cover EDFI-2776's "usage README and required variables"
criterion.
Disclosure: this review was substantially AI-assisted. Please treat the findings as input for human review rather than a final verdict
Keycloak 26.6 supports OpenJDK 17, 21, and 25 (Java 25 is LTS). Update the two "Java 17 or 21" comments in idp-keycloak-setup.ps1 to "17, 21, or 25" so they match the pinned Keycloak 26.6.1 and the installation guide.
Resolve the six findings from the PR #2 review: - Emit HSTS only with a real hostname / CA cert; omit it on the self-signed localhost path so the one-year, port-agnostic pin no longer rewrites Keycloak dev (:8080) to https and breaks login. - Run secret-bearing sqlcmd statements from an Administrators-only temp file via -i instead of -Q, keeping the secret off the process command line (02-prereqs-sql.ps1, 05-deploy-api.ps1 OIDC upsert). - Escape the app password and identifiers in the psql superuser sync (install-all.ps1) to prevent syntax errors and SQL injection. - Quote the container init heredoc and use psql variables so a special character no longer bricks PostgreSQL first boot. - Send Keycloak token requests as hashtable bodies so credentials are URL-encoded. - Fix README quick-start commands to use SecureString parameters and document -AppDbPassword and the PostgreSQL passwords. Also add .gitattributes (*.sh text eol=lf) so the container init script stays LF and cannot be checked out as CRLF under autocrlf.
Thank you! The comments have been addressed. |
The Admin App builds its DB connection string as a URL and interpolates the credentials without URL-encoding them, so a password containing a character that is structural in a URL (or needs percent-encoding) is corrupted at connect time and the API aborts startup with an opaque "Login failed for user". Add a Test-DbPasswordUrlSafe guard that rejects such passwords up front in 02-prereqs-sql.ps1, 05-deploy-api.ps1, and install-all.ps1, with a clear message. Temporary workaround until the Admin App URL-encodes its credentials; password strength is unaffected (CHECK_POLICY still needs any 3 of 4 character categories).
The SQL Server bootstrap no longer enables, resets, or connects as sa. The database and the least-privilege edfi_adminapp login are now provisioned under Windows Authentication, so the installer must run as a Windows account that is a SQL Server sysadmin. The app continues to connect as the dedicated non-sysadmin edfi_adminapp login. - 02-prereqs-sql.ps1: drop -SaPassword and the sa enable/ALTER/verify steps; create the database via Windows Auth. - install-all.ps1: drop -SaPassword; run the post-deploy user/oidc queries as edfi_adminapp (matching the pgsql path). - uninstall.ps1: drop the database and login via Windows Auth only. - 00-check-prereqs.ps1: remove the sa-reset risk checks. - README.md / CONTRIBUTING.md: document the Windows-sysadmin precondition.
The earlier wording pass skipped fenced code blocks and tables; spell out the leftover shorthand there (FE, VM, DB, PG, certs, Windows Auth, least-priv) in README.md and CONTRIBUTING.md.
Windows Server 2019/2022 do not ship winget, which the scripts use to install Node.js, OpenJDK, SQL Server, and Git. Document it as a prerequisite in the README (install it first on Server, at your own risk), have 00-check-prereqs.ps1 flag when it is missing, and fail early with clear guidance at each point of use (setup-vm-prereqs.ps1, 03-prereqs-node.ps1, idp-keycloak-setup.ps1) instead of a cryptic "'winget' is not recognized" mid-install. Also suppress the Invoke-WebRequest progress bar in 01-prereqs-iis.ps1 so the MSI downloads run at full speed on PowerShell 5.1.
The URL-safe guard now also validates the database username (-AppDbUsername / -PgDbUsername / -PostgresAppUser), not only the password: the same characters break the username in the app's URL-form connection string. Reuses Test-DbPasswordUrlSafe and neutralizes its message to cover both credentials.


Summary
This PR imports the
windows-installscript suite into the dedicated Admin App Installation Scripts repository. The scripts fully automate an end-to-end Windows / IIS installation of the Ed-Fi Admin App (v4.0.1), supporting either SQL Server or a Dockerized PostgreSQL backing store, and Keycloak, Entra ID, or Google Workspace as the OIDC identity provider. TLS is on by default, the API runs under the httpPlatform handler, the app connects with a least-privilege database login and a per-install data-encryption key is generated. The suite was validated end to end against Admin App v4.0.1 across both database engines and all three identity providers before this import.Ticket
Type of Change
What Changed
windows-install/— the phased installer and supporting scripts:install-all.ps1— master installer (three phases: prereqs, build, deploy), fully automated.00-check-prereqs.ps1…06-deploy-fe.ps1— per-section scripts (SQL / IIS / Node prereqs, build, API deploy, FE deploy) that can also be run individually.idp-keycloak-setup.ps1/idp-keycloak-start.ps1— local Keycloak provisioning and startup (optional reboot-survival scheduled task).uninstall.ps1/uninstall-keycloak.ps1— clean teardown.setup-vm-prereqs.ps1— fresh-VM bootstrap.yopass-docker.ps1— optional one-time-credential-link stack.README.md— full install guide (prerequisites, TLS/HTTPS, OIDC providers, upstream TLS verification, next steps, troubleshooting).windows-install/docker/— the Docker Compose stack for the PostgreSQL and optional Yopass paths:docker-compose.yml,docker-compose.yopass.yml,.env.example,init/01-create-adminapp-user.sh(least-privilege PostgreSQL role), and aREADME.md..gitignore— ignores the installer-generateddocker/.env(holds database secrets),install-summary.txt, and Node build artifacts.The installer-generated
docker/.envis intentionally excluded;docker/.env.exampledocuments the required variables.Architectural Decisions
sa/ non-superuser login (edfi_adminappon SQL Server asdb_ownerofsbaa;edfiadminappon PostgreSQL owningpublicwithCONNECT+CREATEonly, never a DB-wideGRANT ALL).oidcrow id (no hardcoded/callback/1). Keycloak is the local example provider.127.0.0.1), with an opt-in for remote exposure.Testing
Automated
None — these are deployment / orchestration scripts with no unit-testable surface. All scripts are AST-parse-clean. Validation is end-to-end and manual (below).
Manual — Full E2E matrix (against Admin App v4.0.1, on Windows 11 + IIS)
All 15 paths executed on a clean box; every path listed passed. The code in this PR already incorporates the fixes surfaced during E2E (see Known Limitations).
oidcrow not duplicated, Keycloak client reconciled)GRANT ALL)E2E highlights:
→ 401, FE→ 200with SPA fallback.Content-Security-Policy,Referrer-Policy,X-Content-Type-Options: nosniff,X-Frame-Options: DENY;X-Powered-Byabsent.callback/<id>matches the actualoidcrow id on both engines.https://localhost:3443custom-port redirect at registration.Known Limitations / Follow-Up
myaccount.microsoft.com,myaccount.google.com). Not a blocker for this suite.SSL_VERIFICATIONdefault, remove an unused management secret) is tracked as EDFI-2813 against the Admin App repository; it lands in Admin App v4.1 and is not required for a v4.0.1 install (the installer injects config viaNODE_CONFIGat deploy time).Checklist
docker/.env) excluded from version control