FForm Platform
enzh-CN

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.