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.
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,
};