NexusExamples
Security Best Practices Examples
Code examples showing secure vs insecure FortiBlox API usage
Security Best Practices: Code Examples
Learn by example with clear comparisons of insecure vs secure code patterns for FortiBlox Nexus integration.
Quick Reference
| Pattern | ❌ Insecure | ✅ Secure |
|---|---|---|
| API Keys | Hardcoded in source | Environment variables |
| Browser Apps | Direct API calls | Backend proxy |
| Access Control | No restrictions | Domain/IP restrictions |
| Key Management | Single key everywhere | Separate keys per environment |
| Error Handling | Expose stack traces | Generic error messages |
1. Environment Variables
❌ Don't Do This
// index.js - INSECURE!
const { Connection } = require('@solana/web3.js');
// API key visible in source code
const API_KEY = "fbx_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p";
const connection = new Connection(
`https://nexus.fortiblox.com/rpc?api-key=${API_KEY}`,
'confirmed'
);
async function main() {
const slot = await connection.getSlot();
console.log('Current slot:', slot);
}
main();Problems:
- API key is visible in git commits
- Anyone with code access has your key
- Can't rotate key without code changes
- Key appears in logs and error messages
✅ Do This Instead
// index.js - SECURE
require('dotenv').config();
const { Connection } = require('@solana/web3.js');
// Validate environment variable exists
if (!process.env.FORTIBLOX_API_KEY) {
console.error('Error: FORTIBLOX_API_KEY environment variable is required');
process.exit(1);
}
// Use environment variable
const connection = new Connection(
`https://nexus.fortiblox.com/rpc?api-key=${process.env.FORTIBLOX_API_KEY}`,
'confirmed'
);
async function main() {
try {
const slot = await connection.getSlot();
console.log('Current slot:', slot);
} catch (error) {
console.error('RPC error:', error.message);
process.exit(1);
}
}
main();.env file:
# .env (add to .gitignore!)
FORTIBLOX_API_KEY=fbx_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p
NODE_ENV=production.gitignore:
.env
.env.local
.env.*.local
*.env
node_modules/.env.example (commit this):
# .env.example - Template for environment variables
FORTIBLOX_API_KEY=your_api_key_here
NODE_ENV=development# main.py - SECURE
import os
import sys
from dotenv import load_dotenv
from solana.rpc.api import Client
# Load environment variables
load_dotenv()
# Validate environment variable exists
api_key = os.getenv('FORTIBLOX_API_KEY')
if not api_key:
print('Error: FORTIBLOX_API_KEY environment variable is required', file=sys.stderr)
sys.exit(1)
# Use environment variable
client = Client(f"https://nexus.fortiblox.com/rpc?api-key={api_key}")
def main():
try:
slot = client.get_slot()
print(f'Current slot: {slot.value}')
except Exception as error:
print(f'RPC error: {error}', file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()// main.rs - SECURE
use solana_client::rpc_client::RpcClient;
use std::env;
use dotenv::dotenv;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load environment variables
dotenv().ok();
// Validate environment variable exists
let api_key = env::var("FORTIBLOX_API_KEY")
.expect("FORTIBLOX_API_KEY environment variable is required");
// Use environment variable
let url = format!("https://nexus.fortiblox.com/rpc?api-key={}", api_key);
let client = RpcClient::new(url);
// Make request
match client.get_slot() {
Ok(slot) => println!("Current slot: {}", slot),
Err(error) => {
eprintln!("RPC error: {}", error);
std::process::exit(1);
}
}
Ok(())
}Setup instructions:
# Install dependencies
npm install dotenv # JavaScript
pip install python-dotenv # Python
# Add dotenv = "0.15" to Cargo.toml for Rust
# Create .env file
echo "FORTIBLOX_API_KEY=fbx_your_key_here" > .env
# Add .env to .gitignore
echo ".env" >> .gitignore
# Run your application
node index.js # JavaScript
python main.py # Python
cargo run # Rust2. Browser Applications
❌ Don't Do This
<!-- index.html - EXTREMELY INSECURE! -->
<!DOCTYPE html>
<html>
<head>
<title>My X1 Blockchain App</title>
<script src="https://unpkg.com/@solana/web3.js@latest/lib/index.iife.min.js"></script>
</head>
<body>
<h1>X1 Blockchain Wallet Tracker</h1>
<div id="balance"></div>
<script>
// DANGER: API key visible to anyone who opens DevTools!
const API_KEY = 'fbx_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p';
const connection = new solanaWeb3.Connection(
`https://nexus.fortiblox.com/rpc?api-key=${API_KEY}`,
'confirmed'
);
async function getBalance() {
const publicKey = new solanaWeb3.PublicKey('...');
const balance = await connection.getBalance(publicKey);
document.getElementById('balance').textContent = `Balance: ${balance / 1e9} SOL`;
}
getBalance();
</script>
</body>
</html>Why this is dangerous:
- Open DevTools → Network tab → See API key in URL
- View source → API key in plain text
- Anyone can copy and abuse your key
- No way to revoke without changing code
✅ Do This Instead: Backend Proxy
// server.js - SECURE backend proxy
const express = require('express');
const fetch = require('node-fetch');
require('dotenv').config();
const app = express();
app.use(express.json());
// CORS - restrict to your domain
app.use((req, res, next) => {
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com',
'http://localhost:3000' // Development only
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
// Rate limiting per IP
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 60, // 60 requests per minute
message: { error: 'Too many requests, please try again later' }
});
app.use('/api/rpc', limiter);
// RPC proxy endpoint
app.post('/api/rpc', async (req, res) => {
try {
// Validate request
if (!req.body.method) {
return res.status(400).json({ error: 'Missing RPC method' });
}
// Optional: Filter allowed methods
const allowedMethods = [
'getAccountInfo',
'getBalance',
'getSlot',
'getBlockHeight',
'getLatestBlockhash',
'sendTransaction'
];
if (!allowedMethods.includes(req.body.method)) {
return res.status(403).json({ error: 'Method not allowed' });
}
// API key is stored securely on server
const API_KEY = process.env.FORTIBLOX_API_KEY;
// Forward to FortiBlox
const response = await fetch(
`https://nexus.fortiblox.com/rpc?api-key=${API_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req.body)
}
);
const data = await response.json();
// Return response to client
res.json(data);
} catch (error) {
console.error('RPC proxy error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`RPC proxy running on port ${PORT}`);
});// src/App.js - SECURE frontend (no API key!)
import { useState, useEffect } from 'react';
import { PublicKey } from '@solana/web3.js';
function App() {
const [balance, setBalance] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchBalance();
}, []);
async function fetchBalance() {
try {
setLoading(true);
setError(null);
// Call our backend proxy (NO API KEY!)
const response = await fetch('/api/rpc', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'getBalance',
params: [
'YourPublicKeyHere...'
]
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error.message);
}
setBalance(data.result.value / 1e9);
} catch (err) {
console.error('Error fetching balance:', err);
setError(err.message);
} finally {
setLoading(false);
}
}
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>X1 Blockchain Wallet Tracker</h1>
<p>Balance: {balance} SOL</p>
<button onClick={fetchBalance}>Refresh</button>
</div>
);
}
export default App;// pages/api/rpc.js - SECURE Next.js API route
export default async function handler(req, res) {
// Only allow POST requests
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
// Validate request
if (!req.body.method) {
return res.status(400).json({ error: 'Missing RPC method' });
}
// Optional: Verify origin
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null
].filter(Boolean);
const origin = req.headers.origin;
if (origin && !allowedOrigins.includes(origin)) {
return res.status(403).json({ error: 'Origin not allowed' });
}
// API key from environment (SECURE!)
const API_KEY = process.env.FORTIBLOX_API_KEY;
// Forward to FortiBlox
const response = await fetch(
`https://nexus.fortiblox.com/rpc?api-key=${API_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req.body)
}
);
const data = await response.json();
res.status(200).json(data);
} catch (error) {
console.error('RPC proxy error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}Frontend code:
// pages/index.js
import { useState, useEffect } from 'react';
export default function Home() {
const [balance, setBalance] = useState(null);
async function fetchBalance() {
// Call our API route (NO API KEY!)
const response = await fetch('/api/rpc', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'getBalance',
params: ['YourPublicKeyHere...'],
id: 1
})
});
const data = await response.json();
setBalance(data.result.value / 1e9);
}
useEffect(() => {
fetchBalance();
}, []);
return <div>Balance: {balance} SOL</div>;
}3. Separate Keys Per Environment
❌ Don't Do This
// config.js - INSECURE: Same key everywhere
const config = {
apiKey: process.env.FORTIBLOX_API_KEY, // One key for dev, staging, prod
rpcUrl: 'https://nexus.fortiblox.com/rpc'
};
// Problem: If dev key leaks, production is compromised
// Problem: Can't separate billing per environment
// Problem: Can't apply different restrictions per environment✅ Do This Instead
// config.js - SECURE: Environment-specific keys
function getConfig() {
const env = process.env.NODE_ENV || 'development';
const configs = {
development: {
apiKey: process.env.FORTIBLOX_API_KEY_DEV,
rpcUrl: 'https://nexus.fortiblox.com/rpc',
network: 'devnet',
logLevel: 'debug'
},
staging: {
apiKey: process.env.FORTIBLOX_API_KEY_STAGING,
rpcUrl: 'https://nexus.fortiblox.com/rpc',
network: 'mainnet',
logLevel: 'info'
},
production: {
apiKey: process.env.FORTIBLOX_API_KEY_PROD,
rpcUrl: 'https://nexus.fortiblox.com/rpc',
network: 'mainnet',
logLevel: 'error'
}
};
const config = configs[env];
// Validate API key exists
if (!config.apiKey) {
throw new Error(`FORTIBLOX_API_KEY_${env.toUpperCase()} is not set`);
}
return config;
}
module.exports = getConfig();.env files:
# .env.development
FORTIBLOX_API_KEY_DEV=fbx_test_...
NODE_ENV=development
# .env.staging
FORTIBLOX_API_KEY_STAGING=fbx_test_...
NODE_ENV=staging
# .env.production
FORTIBLOX_API_KEY_PROD=fbx_...
NODE_ENV=productionUsage:
const config = require('./config');
const connection = new Connection(
`${config.rpcUrl}?api-key=${config.apiKey}&network=${config.network}`,
'confirmed'
);Benefits:
- Separate billing per environment
- Revoke one key without affecting others
- Different access controls per environment
- Clear separation of concerns
4. Error Handling
❌ Don't Do This
// INSECURE: Exposing sensitive information in errors
app.post('/api/rpc', async (req, res) => {
try {
const response = await fetch(
`https://nexus.fortiblox.com/rpc?api-key=${process.env.FORTIBLOX_API_KEY}`,
{
method: 'POST',
body: JSON.stringify(req.body)
}
);
const data = await response.json();
res.json(data);
} catch (error) {
// DANGER: Exposing full error to client
res.status(500).json({
error: error.message,
stack: error.stack, // Stack trace visible
apiKey: process.env.FORTIBLOX_API_KEY // API key leaked!
});
}
});✅ Do This Instead
// SECURE: Safe error handling
const winston = require('winston');
// Configure logger (server-side only)
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
app.post('/api/rpc', async (req, res) => {
try {
const response = await fetch(
`https://nexus.fortiblox.com/rpc?api-key=${process.env.FORTIBLOX_API_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req.body)
}
);
if (!response.ok) {
throw new Error(`FortiBlox API returned ${response.status}`);
}
const data = await response.json();
res.json(data);
} catch (error) {
// Log detailed error SERVER-SIDE ONLY
logger.error('RPC proxy error', {
error: error.message,
stack: error.stack,
requestBody: req.body,
timestamp: new Date().toISOString()
});
// Return generic error to client (SAFE)
res.status(500).json({
success: false,
error: 'An error occurred processing your request',
error_code: 'INTERNAL_SERVER_ERROR',
timestamp: Date.now()
});
}
});Monitoring setup:
// Integrate with error tracking service
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
// Filter sensitive data
beforeSend(event) {
// Remove API keys from error reports
if (event.extra) {
delete event.extra.apiKey;
}
return event;
}
});
app.use(Sentry.Handlers.errorHandler());5. Access Control
❌ Don't Do This
// INSECURE: No access control
app.post('/api/rpc', async (req, res) => {
// Anyone can call this!
// No domain restrictions
// No rate limiting
// No IP validation
const response = await fetch(`https://nexus.fortiblox.com/rpc?api-key=${process.env.FORTIBLOX_API_KEY}`, {
method: 'POST',
body: JSON.stringify(req.body)
});
res.json(await response.json());
});✅ Do This Instead
// SECURE: Multiple layers of access control
const express = require('express');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const app = express();
// 1. CORS - Domain restrictions
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com',
'https://*.myapp.com', // Wildcard subdomain
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null
].filter(Boolean);
if (!origin || allowedOrigins.some(allowed => {
if (allowed.includes('*')) {
const regex = new RegExp(allowed.replace('*', '.*'));
return regex.test(origin);
}
return allowed === origin;
})) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
};
app.use(cors(corsOptions));
// 2. Rate Limiting - Per IP
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 60, // 60 requests per minute
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please slow down' },
// Store in Redis for distributed rate limiting
store: new RedisStore({
client: redisClient,
prefix: 'rl:'
})
});
app.use('/api/rpc', apiLimiter);
// 3. IP Whitelist (optional for server-to-server)
function ipWhitelist(req, res, next) {
// Skip if not enabled
if (!process.env.IP_WHITELIST_ENABLED) {
return next();
}
const clientIp = req.ip || req.headers['x-forwarded-for'];
const allowedIps = (process.env.ALLOWED_IPS || '').split(',');
if (allowedIps.includes(clientIp)) {
next();
} else {
res.status(403).json({
error: 'IP address not allowed',
your_ip: clientIp
});
}
}
app.use('/api/rpc', ipWhitelist);
// 4. Request Validation
function validateRequest(req, res, next) {
const { method, params } = req.body;
// Validate JSON-RPC structure
if (!method || !Array.isArray(params)) {
return res.status(400).json({
error: 'Invalid JSON-RPC request'
});
}
// Whitelist allowed methods
const allowedMethods = [
'getAccountInfo',
'getBalance',
'getSlot',
'getBlockHeight',
'sendTransaction'
];
if (!allowedMethods.includes(method)) {
return res.status(403).json({
error: 'RPC method not allowed',
allowed_methods: allowedMethods
});
}
next();
}
app.post('/api/rpc', validateRequest, async (req, res) => {
try {
const response = await fetch(
`https://nexus.fortiblox.com/rpc?api-key=${process.env.FORTIBLOX_API_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req.body)
}
);
const data = await response.json();
res.json(data);
} catch (error) {
logger.error('RPC error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});Complete Secure Application Example
Here's a complete, production-ready example combining all best practices:
// server.js - Production-ready secure backend
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const winston = require('winston');
const fetch = require('node-fetch');
// Validate required environment variables
const requiredEnvVars = ['FORTIBLOX_API_KEY', 'NODE_ENV'];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
console.error(`Error: ${envVar} environment variable is required`);
process.exit(1);
}
}
// Configure logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.Console({
format: winston.format.simple(),
level: process.env.NODE_ENV === 'production' ? 'error' : 'debug'
})
]
});
const app = express();
// Security headers
app.use(helmet());
// Body parsing
app.use(express.json({ limit: '10kb' }));
// CORS
const corsOptions = {
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true
};
app.use(cors(corsOptions));
// Rate limiting
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 60,
message: { error: 'Rate limit exceeded. Please try again later.' }
});
app.use('/api/', limiter);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// RPC proxy endpoint
app.post('/api/rpc', async (req, res) => {
const requestId = Math.random().toString(36).substring(7);
try {
logger.info('RPC request', {
requestId,
method: req.body.method,
ip: req.ip
});
// Validate request
if (!req.body.method) {
return res.status(400).json({
error: 'Missing RPC method',
error_code: 'INVALID_REQUEST'
});
}
// Forward to FortiBlox
const response = await fetch(
`https://nexus.fortiblox.com/rpc?api-key=${process.env.FORTIBLOX_API_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req.body),
timeout: 30000
}
);
if (!response.ok) {
throw new Error(`FortiBlox API returned ${response.status}`);
}
const data = await response.json();
logger.info('RPC response', { requestId, success: true });
res.json(data);
} catch (error) {
logger.error('RPC error', {
requestId,
error: error.message,
stack: error.stack
});
res.status(500).json({
error: 'Internal server error',
error_code: 'INTERNAL_ERROR',
request_id: requestId
});
}
});
// Error handler
app.use((error, req, res, next) => {
logger.error('Unhandled error', { error: error.message, stack: error.stack });
res.status(500).json({ error: 'Internal server error' });
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully');
server.close(() => {
logger.info('Server closed');
process.exit(0);
});
});// src/App.js - Production-ready secure frontend
import { useState, useEffect } from 'react';
// API client with error handling
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async rpcCall(method, params = []) {
try {
const response = await fetch(`${this.baseURL}/api/rpc`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method,
params,
id: Date.now()
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || `HTTP ${response.status}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error.message);
}
return data.result;
} catch (error) {
console.error('RPC error:', error);
throw error;
}
}
}
const apiClient = new APIClient(process.env.REACT_APP_API_URL || '');
function App() {
const [balance, setBalance] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
async function fetchBalance() {
try {
setLoading(true);
setError(null);
const result = await apiClient.rpcCall('getBalance', [
'YourPublicKeyHere...'
]);
setBalance(result.value / 1e9);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
useEffect(() => {
fetchBalance();
}, []);
return (
<div className="App">
<h1>Secure X1 Blockchain App</h1>
{loading && <p>Loading...</p>}
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
{balance !== null && <p>Balance: {balance} SOL</p>}
<button onClick={fetchBalance} disabled={loading}>
Refresh
</button>
</div>
);
}
export default App;# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
RUN npm ci --only=production
# Copy source
COPY . .
# Production image
FROM node:18-alpine
WORKDIR /app
# Copy from builder
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app .
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
CMD ["node", "server.js"]# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
env_file:
- .env.production
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run security audit
run: npm audit --audit-level=high
- name: Scan for secrets
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
test:
runs-on: ubuntu-latest
needs: security-scan
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
env:
FORTIBLOX_API_KEY: ${{ secrets.FORTIBLOX_API_KEY_TEST }}
deploy:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- name: Deploy to production
run: |
# Your deployment commands here
echo "Deploying..."
env:
FORTIBLOX_API_KEY: ${{ secrets.FORTIBLOX_API_KEY_PROD }}Security Checklist
Use this checklist for every FortiBlox integration:
Development
- API keys in environment variables (not hardcoded)
-
.envfiles in.gitignore -
.env.exampletemplate provided - Separate keys for dev/staging/prod
- Keys validated on startup
Browser Applications
- Backend proxy implemented
- No API keys in frontend code
- CORS properly configured
- Request validation implemented
- Rate limiting enabled
Access Control (Developer Portal)
- Domain restrictions enabled (web apps)
- IP restrictions enabled (server apps)
- Network restrictions configured
- Least privilege applied
Error Handling
- Sensitive data not exposed in errors
- Detailed logging server-side only
- Generic errors sent to clients
- Error tracking configured
Monitoring
- Usage alerts configured
- Suspicious activity monitored
- Key rotation schedule documented
- Incident response plan ready
Deployment
- Environment variables in secrets manager
- Security headers enabled (Helmet)
- HTTPS enforced
- Health checks configured
- Logging aggregation set up