Security and deployment
Deploy FormPlatform as a private .NET 10 service with protected secrets, HTTPS and operational guardrails.
Secure configuration and deployment
FormPlatform uses the standard ASP.NET Core configuration hierarchy plus one
deployment-owned JSON secret source. No production credential should be stored
in the repository or publish directory.
Configuration precedence
At startup configuration is assembled in this order:
1. `appsettings.json` and `appsettings.{Environment}.json`;
2. .NET User Secrets in Development;
3. environment variables and command-line configuration added by ASP.NET Core;
4. one deployment secret JSON source:
- systemd: `$CREDENTIALS_DIRECTORY/formplatform-secrets.json`;
- other hosts, including IIS: the absolute path in
`FORMPLATFORM_SECRETS_FILE`.
The deployment secret document is intentionally last and authoritative. Put
secrets only in it; keep normal operational settings in `appsettings.json`, an
environment-specific appsettings file, or environment variables. Startup fails
immediately if a configured secret file is missing or invalid. Configuring both
secret-source mechanisms is rejected to avoid ambiguous precedence.
`appsettings.json` contains empty secret placeholders. `DataAccess` also has
`IncludeSqlParameterValues=false` so production logs do not contain submitted
answers, tokens, or other SQL parameter values.
Immediate credential rotation
Credentials previously stored in the tracked `appsettings.json` must be
considered exposed even after the file is cleaned. Rotate at least:
- the PostgreSQL password;
- Google and Facebook client secrets;
- PayPal client secret and any webhook credential;
- any value copied from an earlier Git revision, build artifact, backup, log,
or chat transcript.
Rotation is the security fix. Rewriting Git history may reduce accidental
discovery, but it does not revoke a credential that has already escaped.
Local development with User Secrets
`src/FormPlatform.Host/FormPlatform.csproj` has a stable `UserSecretsId`, so no
initialization command is required. From the repository root, set
development-only values with `--project`:
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "ManagementDatabase:ConnectionString" 'Host=localhost;Port=5432;Database=formplatform;Username=formplatform_dev;Password=CHANGE_ME'
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "FormStorage:ConnectionString" 'Host=localhost;Port=5432;Database=formplatform;Username=formplatform_dev;Password=CHANGE_ME'
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "RespondentExternalAuthentication:Google:Enabled" "true"
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "RespondentExternalAuthentication:Google:ClientId" "CHANGE_ME"
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "RespondentExternalAuthentication:Google:ClientSecret" "CHANGE_ME"
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "RespondentExternalAuthentication:Facebook:Enabled" "true"
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "RespondentExternalAuthentication:Facebook:ClientId" "CHANGE_ME"
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "RespondentExternalAuthentication:Facebook:ClientSecret" "CHANGE_ME"
Other useful commands:
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj list
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj remove "RespondentExternalAuthentication:Google:ClientSecret"
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj clear
User Secrets are outside the project tree but are not encrypted. They are for a
developer workstation only, never for Production. Each developer should use
separate development credentials.
The maintenance scripts `tools/sync-system-forms.js` and
`tools/sync-form-center.js` use the same sources. They resolve an explicit
`--connection` first, then deployment secret JSON, the standard
`ManagementDatabase__ConnectionString` environment variable, User Secrets, and
finally appsettings. Prefer User Secrets locally; command-line connection
strings can be exposed through shell history or process inspection.
Secret JSON schema
Copy `deploy/secrets/formplatform-secrets.example.json` outside the repository
and replace placeholders. The JSON hierarchy is the same hierarchy used by
`appsettings.json`. Sections and properties not present in the secret file keep
their value from the earlier configuration sources.
Do not place the real copy under `deploy/`, `/var/www/FormPlatform`, the IIS site
directory, or another Git working tree.
Payment private keys and certificates follow the same rule. On systemd, load
them as additional credentials and configure their runtime credential paths; on
IIS, keep them under a separate ACL-protected directory readable only by the
application-pool identity.
Raspberry Pi and systemd
Use a 64-bit Raspberry Pi OS installation. The example is a framework-dependent
deployment and expects the ASP.NET Core 10 runtime:
dotnet --info
# After configuring Microsoft's package repository for the installed Debian release:
sudo apt update
sudo apt install aspnetcore-runtime-10.0 nginx
Publish on the development machine and copy the result to the server:
dotnet publish -c Release -o .\publish
Create the service identity and writable directories:
sudo useradd --system --home /var/www/FormPlatform --shell /usr/sbin/nologin formplatform
sudo mkdir -p /var/www/FormPlatform /var/www/FormPlatform/App_Data /var/www/FormPlatform/Logs
sudo chown -R root:root /var/www/FormPlatform
sudo chown formplatform:formplatform /var/www/FormPlatform/App_Data /var/www/FormPlatform/Logs
sudo chmod 750 /var/www/FormPlatform/App_Data /var/www/FormPlatform/Logs
Copy published files into `/var/www/FormPlatform`, keeping application binaries
owned by root and not writable by the service account.
Install the real secret document outside the app:
sudo install -d -o root -g root -m 700 /etc/formplatform
sudo install -o root -g root -m 600 formplatform-secrets.json \
/etc/formplatform/formplatform-secrets.json
Install and start the included unit:
sudo cp deploy/systemd/formplatform.service /etc/systemd/system/formplatform.service
sudo systemctl daemon-reload
sudo systemctl enable --now formplatform
sudo systemctl status formplatform
sudo journalctl -u formplatform -f
`LoadCredential=` copies the root-owned JSON into a service-private read-only
runtime directory. systemd sets `CREDENTIALS_DIRECTORY`; FormPlatform loads the
fixed filename `formplatform-secrets.json` from that directory. The service
account does not need permission to read the original `/etc` file.
On a systemd release supporting encrypted credentials, the plaintext source can
be replaced by a `systemd-creds encrypt` output and
`LoadCredentialEncrypted=`. Confirm the installed systemd version and recovery
strategy before adopting host-bound encryption. An external Vault remains the
better choice for multiple hosts and centralized rotation.
Nginx and HTTPS
Copy `deploy/nginx/formplatform.conf` into the Nginx `http` context/site
configuration, replace every `forms.example.com` value in the Nginx and systemd
templates and replace certificate paths, then verify:
sudo nginx -t
sudo systemctl reload nginx
Kestrel listens only on `127.0.0.1:5080`; Nginx owns public ports 80/443. The
service enables forwarded headers so OAuth callback URLs and HTTPS-aware links
use the original public scheme. Restrict PostgreSQL to localhost/private
networks and open only required firewall ports.
The service grants write access only to `App_Data` and `Logs`. `App_Data/keys`
contains ASP.NET Core Data Protection keys; preserve it across deployments or
existing authentication cookies and protected tokens become unreadable.
Windows Server and IIS
1. Install IIS, then install the matching .NET 10 Hosting Bundle. If IIS was
installed after the Hosting Bundle, repair/reinstall the bundle.
2. Publish with `dotnet publish -c Release -o .\publish` and deploy to a folder
such as `C:\inetpub\FormPlatform`.
3. Create a dedicated application pool named `FormPlatform`, use
`ApplicationPoolIdentity`, set `.NET CLR Version` to `No Managed Code`, and
disable 32-bit applications for a 64-bit deployment.
4. Use an explicit HTTPS host binding and certificate. Avoid a top-level
wildcard binding.
Place the real secret file outside the site and lock its ACL:
New-Item -ItemType Directory -Force C:\ProgramData\FormPlatform
Copy-Item .\formplatform-secrets.json C:\ProgramData\FormPlatform\formplatform-secrets.json
icacls C:\ProgramData\FormPlatform /inheritance:r
icacls C:\ProgramData\FormPlatform /grant:r `
"SYSTEM:(OI)(CI)(F)" `
"BUILTIN\Administrators:(OI)(CI)(F)" `
"IIS AppPool\FormPlatform:(OI)(CI)(R)"
Grant the application pool read/execute permission on the published application
and Modify permission only on runtime-write directories:
New-Item -ItemType Directory -Force C:\inetpub\FormPlatform\App_Data
New-Item -ItemType Directory -Force C:\inetpub\FormPlatform\Logs
icacls C:\inetpub\FormPlatform /grant:r "IIS AppPool\FormPlatform:(OI)(CI)(RX)"
icacls C:\inetpub\FormPlatform\App_Data /grant:r "IIS AppPool\FormPlatform:(OI)(CI)(M)"
icacls C:\inetpub\FormPlatform\Logs /grant:r "IIS AppPool\FormPlatform:(OI)(CI)(M)"
The SDK generates `web.config` during publish. Merge the three environment
environment variables from `deploy/iis/web.config.example` into the generated
`<aspNetCore><environmentVariables>` element:
<environmentVariable name="DOTNET_ENVIRONMENT" value="Production" />
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
<environmentVariable name="AllowedHosts" value="forms.example.com" />
<environmentVariable name="Smtp__PublicBaseUrl" value="https://forms.example.com" />
<environmentVariable name="Commerce__PublicBaseUrl" value="https://forms.example.com" />
<environmentVariable name="FORMPLATFORM_SECRETS_FILE"
value="C:\ProgramData\FormPlatform\formplatform-secrets.json" />
The environment variable contains only a path, not a credential. At startup the
application requires an absolute path and reads the ACL-protected JSON. Do not
put secret values directly into `web.config`, IIS Manager, or command-line
arguments.
Replace `forms.example.com` with the real public host. Register the resulting
HTTPS Google/Facebook callback URLs with their providers before enabling those
providers.
Recycle the application pool after changing the secret file. The secret source
uses `reloadOnChange=false`; controlled restart/recycle makes rotation explicit
and avoids partially observed configuration.
For a web farm, share Data Protection keys through a protected durable store and
use a central secret manager rather than copying the JSON to every server.
Verification checklist
After configuring either host:
1. Start the application and confirm it reaches the management database.
2. Confirm Google/Facebook provider availability matches the secret JSON.
3. Complete one login, restart/recycle, and confirm the session remains valid.
4. Verify the application identity cannot modify binaries or read unrelated
server files.
5. Verify logs contain SQL text only when enabled and never contain SQL parameter
values or credentials.
6. Confirm the real secret document and payment private keys are absent from Git,
the publish archive, backups intended for developers, and diagnostic bundles.
Official references:
- <https://learn.microsoft.com/aspnet/core/security/app-secrets?view=aspnetcore-10.0>
- <https://learn.microsoft.com/aspnet/core/fundamentals/configuration/?view=aspnetcore-10.0>
- <https://learn.microsoft.com/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-10.0>
- <https://learn.microsoft.com/aspnet/core/host-and-deploy/iis/?view=aspnetcore-10.0>
Trusted private-deployment modules
Customer modules under `Modules` execute inside FormPlatform and therefore have
the same OS and database permissions as the application. Install only reviewed
modules, keep the directory writable only by the deployment administrator, and
never expose a web endpoint that uploads DLLs. Module-specific secrets use the
same User Secrets, systemd credential or IIS external secret source described in
this guide. Never place secrets in a client extension JavaScript file.
See private deployment extension architecture for server and client
extension packaging.