Coming up! Version 0.7-preview is out now! Check out the release notes.

On this page

Security

Security in Portway is layered: tokens decide who gets in, scopes and environments decide what they can reach, and network rules decide where requests may go.

Note

Treat this page as a starting point rather than a policy. Align it with your organisation's security policies before exposing Portway to production traffic.

Authentication

All API requests require a Bearer token:

http
Authorization: Bearer your-token-here

Tokens are generated using cryptographically secure random values and stored encrypted on disk. Each token is bound to a username for audit trail purposes.

First-run token

On first run, Portway generates an initial token and writes it to tokens/YOUR_SERVER_NAME.txt. The token generator reference shows the file format and the fields it carries.

Caution

This file is highly sensitive: it carries a token with full scope and environment access. Remove it from disk immediately after recording the token somewhere secure. Use the Web UI to manage all subsequent tokens.

Authorization

Scope control

Restrict a token to specific endpoints using the AllowedScopes field:

Pattern Access
* All endpoints
Products,Orders Named endpoints only
Product* All endpoints matching the prefix
Company/Employees Specific namespaced endpoint
Company/* All endpoints in a namespace
GET:Products Single endpoint, single method

Environment control

Restrict a token to specific environments using AllowedEnvironments:

Pattern Access
* All environments
prod Single environment
dev,test Named environments
dev* All environments matching the prefix

Endpoint-level restrictions

Individual endpoints enforce their own environment and visibility constraints:

json
{
  "DatabaseObjectName": "SensitiveData",
  "AllowedEnvironments": ["prod"],
  "Hidden": true,
  "AllowedMethods": ["GET"]
}

Both token-level and endpoint-level restrictions must pass for a request to succeed. See Environments, access control for the full matrix.

Network security

IP restrictions

Configure allowed hosts and blocked IP ranges in environments/network-access-policy.json:

json
{
  "allowedHosts": [
    "localhost",
    "127.0.0.1",
    "your-internal-server.local"
  ],
  "blockedIpRanges": [
    "10.0.0.0/8",
    "172.16.0.0/12",
    "192.168.0.0/16"
  ]
}

Security headers

Portway adds these headers to all responses automatically:

Header Value
X-Content-Type-Options nosniff
X-Frame-Options DENY
Strict-Transport-Security max-age=31536000
Referrer-Policy strict-origin-when-cross-origin
Content-Security-Policy default-src 'self'; object-src 'none'; frame-ancestors 'none'; ...

Console pages at /ui add Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy, both same-origin. See Headers for the full set.

Secrets management

Automatic encryption

Portway encrypts plaintext secrets in settings.json files on next startup. Connection strings and authentication values written in plaintext become PWENC:... format.

Automatic encryption applies only to per-environment settings.json files and the MCP configuration store (mcp.db). It does not rewrite appsettings.json, so values placed there (such as WebUi:AdminApiKey) stay in plaintext.

Web UI accounts

Web UI accounts live in auth.db, with passwords hashed using PBKDF2-SHA256.

On the first start with no accounts, an existing WebUi:AdminApiKey becomes the account admin, with the key as its password. After that the setting is no longer read for sign-in and can be removed.

Never store a real admin key in appsettings.json. The shipped file intentionally contains the placeholder INSECURE-CHANGE-ME-admin-api-key, which Portway rejects in production: no account is seeded from it and an error is logged.

Supply the key through the environment instead:

yaml
# docker-compose.yml
environment:
  - WebUi__AdminApiKey=${PORTWAY_ADMIN_KEY}
powershell
[Environment]::SetEnvironmentVariable("WebUi__AdminApiKey", "<your-key>", "Machine")

Generate a strong key (32+ characters; shorter keys log a warning at startup):

bash
openssl rand -base64 48
powershell
[Convert]::ToBase64String((1..48 | ForEach-Object { Get-Random -Maximum 256 }))

With Azure Key Vault configured (KEYVAULT_URI), the key can also be served from the vault through the standard ASP.NET Core configuration pipeline. Environment variables and Key Vault values override anything in appsettings.json.

The Settings page in the Web UI shows the current key strength (not set / placeholder / weak / strong) under Security Posture.

Azure Key Vault

Store connection strings and server names in Azure Key Vault instead of settings.json:

powershell
$env:KEYVAULT_URI = "https://your-keyvault.vault.azure.net/"
bash
export KEYVAULT_URI="https://your-keyvault.vault.azure.net/"

Create secrets named by environment:

  • {environment}-ConnectionString
  • {environment}-ServerName
  • {environment}-Headers (JSON string)

Portway fetches these at startup and uses them identically to file-based configuration.

SQL Server permissions

When using Windows Authentication (NTLM) via IIS, configure the database account with minimum required permissions:

sql
USE [master];
GO
IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = N'DOMAIN\USER_NAME')
BEGIN
    EXEC ('CREATE LOGIN [DOMAIN\USER_NAME] FROM WINDOWS;');
END
GO
USE [YourDatabase];
GO
IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = N'DOMAIN\USER_NAME')
BEGIN
    CREATE USER [DOMAIN\USER_NAME] FOR LOGIN [DOMAIN\USER_NAME];
END
GO
ALTER ROLE [db_datareader] ADD MEMBER [DOMAIN\USER_NAME];  -- Read tables/views
ALTER ROLE [db_datawriter] ADD MEMBER [DOMAIN\USER_NAME];  -- Write (insert/update/delete)
GRANT EXECUTE TO [DOMAIN\USER_NAME];                       -- Stored procedures
GRANT VIEW DEFINITION TO [DOMAIN\USER_NAME];               -- Schema metadata
GO

Grant only the roles your endpoints require. A read-only deployment needs only db_datareader.

Logging and auditing

Security events are logged at Warning or Debug level:

txt
[DBG] Invalid token: {masked}
[WRN] IP {IP} has exceeded rate limit, blocking for {period}

Enable request traffic logging to capture headers and bodies for security analysis:

json
{
  "RequestTrafficLogging": {
    "Enabled": true,
    "CaptureHeaders": true,
    "IncludeRequestBodies": true
  }
}

See Monitoring for traffic logging configuration details.

Recovering an account

Accounts are managed from the shell when nobody can sign in. Run these from the directory Portway runs in, so auth.db is found:

bash
portway accounts list
portway accounts password <username> <new-password>
portway accounts create <username> <password> [administrator|viewer]

promote, demote, enable, disable and delete are available too. Portway refuses any of them that would leave no active administrator.

Account roles

Accounts are either administrator or viewer.

An administrator can change anything the console exposes: settings, endpoints, environments, tokens, and other accounts. A viewer can read all of those pages, change its own password, and link or unlink its own single sign-on identity. Every other write returns 403, including creating accounts, so a viewer cannot promote itself.

Portway reads the role from the database on each write rather than from the session cookie. A demoted account loses write access on its next request, without waiting for its session to expire.

Accounts created before this behavior existed

Earlier builds stored the role but did not enforce it, so anything marked viewer still had full write access. After upgrading, open the Users page and check the accounts listed there.

Sessions are signed with portway.key, written next to auth.db on first use. Deleting that file signs everyone out.

Pre-deployment checklist

  • HTTPS binding configured in IIS
  • IIS Application Pool using minimum-privilege identity
  • Web UI account created with a strong password, and WebUi__AdminApiKey removed once it has been migrated
  • ForwardedHeaders__KnownProxies set to your reverse proxy, so per-IP rate limiting, the sign-in lockout, and the Web UI network gate see real client addresses
  • Account roles reviewed, so anyone who only needs to read the console holds viewer
  • portway.key kept with the deployment and excluded from backups that others can read
  • Azure Key Vault configured (if applicable)
  • Initial token file removed from disk
  • Tokens created with specific scopes and environments
  • Rate limiting configured
  • Firewall rules reviewed
  • Security headers verified with a response inspection tool

Incident response: compromised token

  1. Open the Web UI and navigate to Access Tokens
  2. Click Rotate Token on the affected entry to invalidate it and generate a replacement
  3. Update all applications using the compromised token with the new value
  4. Enable traffic logging if not already active to monitor for continued unauthorized activity:
    json
    {
      "RequestTrafficLogging": { "Enabled": true, "CaptureHeaders": true }
    }
  5. Document the incident for your security audit trail

INFO

Manage all tokens in the Web UI under Tokens. The Tokens page lists all active (non-revoked, non-expired) tokens with their scope and environment restrictions.

WARNING

Token revocation is permanent. A revoked token cannot be reactivated. Create a new token for any affected user or system.

Next steps

Last updated: 2026-09-07