This document describes the security hardening implemented for Stellar network configuration in StreamPay-Frontend. The changes prevent dangerous configuration mistakes that could lead to real asset loss.
For internal worker-to-API authentication and Kubernetes network policy guidance, see docs/internal-service-auth.md.
The following dangerous scenarios are now prevented:
- Using mainnet secret keys against testnet Horizon - Could cause testnet transactions to be signed with production keys
- Using testnet Horizon with production wallet secrets - Could leak production credentials to testnet
- Accidentally defaulting to mainnet when env vars are missing - Could silently route to production
- Displaying testnet balances as if they are real user funds - Financial UX confusion
- Hardcoded network passphrases scattered across handlers - Maintenance risk and security holes
File: app/lib/config/stellar.ts
All Stellar network configuration is centralized in a single module:
- Testnet Profile:
Test SDF Network ; September 2015 - Mainnet Profile:
Public Global Stellar Network ; September 2015 - Horizon URLs, friendbot availability, explorer URLs
- Asset labeling rules for UI safety
Usage:
import { getNetworkProfile } from './config/stellar';
const profile = getNetworkProfile('testnet');
console.log(profile.horizonUrl); // https://horizon-testnet.stellar.orgFile: app/lib/config/index.ts
The validateConfig() function performs strict validation at application boot:
- Required variables:
STELLAR_NETWORK,JWT_SECRET - No silent fallbacks: Missing vars cause immediate failure
- CI guardrails: CI environment cannot use mainnet or production secrets
- JWT_SECRET validation: Must be 32+ characters in production
- Anomaly threshold validation: Must be positive numbers
Bootstrap:
import { bootstrapApplication } from './config/bootstrap';
// Call at application startup
const config = bootstrapApplication();Files: middleware.ts, app/lib/cors.ts, app/lib/config/index.ts
The public API surface is protected by an explicit ALLOWED_ORIGINS allowlist. The value is a comma-separated list of valid origins, such as https://app.partner.com,http://localhost:3000.
ALLOWED_ORIGINSis required for all environments.- In
production, wildcard origin*is not permitted. - Preflight responses use a 10-minute cache (
Access-Control-Max-Age: 600) to avoid breaking deploys while preserving explicit origin validation. - No
Access-Control-Allow-Originheader is reflected for disallowed browser origins. - The API does not enable credentialed CORS by default, so bearer tokens remain the expected auth mechanism.
A public JWKS endpoint is now available at /\.well-known/jwks.json for verification of signed JWTs. The endpoint returns the configured signing keys and their key IDs, enabling downstream consumers to validate tokens without sharing the secret material directly.
To support key rotation, the auth layer now:
- signs new tokens with a configurable
kidheader, - accepts tokens signed with the current secret and an optional previous secret,
- exposes the current and previous keys through the JWKS document when configured.
This allows gradual migration to a new signing secret while preserving compatibility for already-issued tokens.
File: app/lib/logger.ts
All logs automatically redact sensitive values:
- JWT secrets
- Private keys
- Passwords
- Auth tokens
- Seed phrases
Example:
logger.info('Configuration loaded', {
JWT_SECRET: 'my-secret-key', // Automatically redacted to [REDACTED]
PUBLIC_KEY: 'GABC123', // Not redacted
});All hardcoded network passphrases and Horizon URLs have been removed from:
app/lib/assets.ts- Now usesgetConfig().network.horizonUrlapp/api/auth/wallet/route.ts- Now usesgetConfig().jwtSecretapp/api/identity/me/route.ts- Now usesgetConfig().jwtSecretdetector.ts- Now usesgetConfig().anomalyThresholds
File: .github/workflows/ci.yml
CI is enforced to use testnet only:
- Explicit environment variables set for testnet
- Security check step validates no production secrets
- Fails if mainnet configuration detected
- Fails if production JWT_SECRET detected
File: app/components/NetworkBadge.tsx
New components for financial safety:
NetworkBadge- Displays "TESTNET ONLY" warning on testnetAssetLabel- Adds "TESTNET" suffix to asset codes on testnet
Usage:
<NetworkBadge showLabel={true} />
<AssetLabel assetCode="XLM" />| Variable | Purpose | Required | Example |
|---|---|---|---|
STELLAR_NETWORK |
Network selection | Yes | testnet, mainnet |
JWT_SECRET |
JWT signing secret | Yes | 32+ character random string |
| Variable | Purpose | Default | Example |
|---|---|---|---|
SERVICE_NAME |
Service identifier | streampay-frontend |
streampay-frontend |
NODE_ENV |
Environment | development |
production |
INTERNAL_AUTH_TOKEN |
Service-to-service auth | - | Random string |
ANOMALY_CREATION_THRESHOLD |
Fraud detection limit | 50 |
100 |
ANOMALY_SETTLE_THRESHOLD |
Fraud detection limit | 20 |
30 |
- Passphrase:
Test SDF Network ; September 2015 - Horizon URL:
https://horizon-testnet.stellar.org - Friendbot: Available
- Explorer:
https://stellar.expert/testnet - Asset Label:
TESTNET - Production:
false
- Passphrase:
Public Global Stellar Network ; September 2015 - Horizon URL:
https://horizon.stellar.org - Friendbot: Not available
- Explorer:
https://stellar.expert - Asset Label: (empty)
- Production:
true
-
Copy
.env.exampleto.env.local:cp .env.example .env.local
-
Configure for testnet:
STELLAR_NETWORK=testnet JWT_SECRET=dev-secret-key-at-least-32-chars NODE_ENV=development
-
Start the application:
npm run dev
-
Set production environment variables:
STELLAR_NETWORK=mainnet JWT_SECRET=<secure-32-char-random-string> NODE_ENV=production
-
Generate a secure JWT secret:
openssl rand -base64 32
-
Deploy via your hosting platform
Before deploying to production:
-
STELLAR_NETWORKis set tomainnet(if deploying to mainnet) -
JWT_SECRETis at least 32 characters -
JWT_SECRETis NOT the default value -
NODE_ENVis set toproduction - No testnet secrets are used with mainnet configuration
- Horizon URL matches the selected network
- Internal auth tokens are set if using service mesh
- Anomaly thresholds are appropriate for your traffic
- CI is configured to use testnet only
-
.env.localis in.gitignore - No real credentials in
.env.example
The following validation rules are enforced at boot:
- Missing STELLAR_NETWORK → Fail with error
- Missing JWT_SECRET → Fail with error
- Invalid network selection → Fail with error
- JWT_SECRET too short → Fail with error
- Production with default JWT_SECRET → Fail with error
- CI with mainnet network → Fail with error
- CI with production JWT_SECRET → Fail with error
- Invalid anomaly thresholds → Fail with error
STELLAR_NETWORK environment variable is requiredInvalid STELLAR_NETWORK: <value>. Supported networks: testnet, mainnetJWT_SECRET environment variable is requiredJWT_SECRET must be at least 32 characters for securityProduction environment cannot use default JWT_SECRETCI environment detected with mainnet network configurationCI environment detected with production JWT_SECRETANOMALY_CREATION_THRESHOLD must be a positive numberANOMALY_SETTLE_THRESHOLD must be a positive number
Run configuration validation tests:
npm test -- app/lib/config/config.test.tsTest coverage includes:
- Network profile validation
- Required variable validation
- CI guardrail validation
- Anomaly threshold validation
- Secret redaction
- Config caching
Test application boot scenarios:
npm testFix: Set STELLAR_NETWORK=testnet or STELLAR_NETWORK=mainnet in .env.local
Fix: Generate a longer secret using openssl rand -base64 32
Fix: CI is restricted to testnet. Use testnet in CI or deploy manually.
Fix: Set a custom JWT_SECRET when NODE_ENV=production
If you have existing code using hardcoded values:
const horizonUrl = 'https://horizon-testnet.stellar.org';
const jwtSecret = process.env.JWT_SECRET || 'default-secret';import { getConfig } from './config';
const config = getConfig();
const horizonUrl = config.network.horizonUrl;
const jwtSecret = config.jwtSecret;- JWT secrets are never logged (automatically redacted)
- Wallet signing keys are handled by the Stellar SDK (not stored in this frontend)
- No private keys are stored in environment variables
- All secrets are validated at boot
- Network passphrases are centralized to prevent mismatched signing
- Horizon URLs are validated against network selection
- No silent fallback to mainnet under any circumstance
- Testnet assets are clearly labeled in the UI
- Network badges show "TESTNET ONLY" on testnet
- Asset labels include "TESTNET" suffix on testnet
- Production deployments require explicit mainnet configuration
When reviewing changes to network configuration:
- No hardcoded network passphrases in new code
- No hardcoded Horizon URLs in new code
- All config imports from
./config/stellar.ts - No unsafe fallback defaults for secrets
- CI environment variables are testnet-only
- Secret redaction is used in any new logging
- UI safety labels are present on asset displays
- Tests cover new validation rules
- Documentation is updated
The architecture supports adding future Stellar networks:
- Add network profile to
app/lib/config/stellar.ts - Update
StellarNetworktype - Add to
NETWORK_PROFILESregistry - Update documentation
Example:
export const FUTURENET_PROFILE: StellarNetworkProfile = {
name: 'future',
passphrase: 'Future Network Passphrase',
horizonUrl: 'https://horizon-future.stellar.org',
hasFriendbot: true,
explorerUrl: 'https://stellar.expert/future',
assetLabel: 'FUTURE',
isProduction: false,
};