FortiBlox LogoFortiBlox Docs
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 KeysHardcoded in sourceEnvironment variables
Browser AppsDirect API callsBackend proxy
Access ControlNo restrictionsDomain/IP restrictions
Key ManagementSingle key everywhereSeparate keys per environment
Error HandlingExpose stack tracesGeneric 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  # Rust

2. 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:

  1. Open DevTools → Network tab → See API key in URL
  2. View source → API key in plain text
  3. Anyone can copy and abuse your key
  4. 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=production

Usage:

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)
  • .env files in .gitignore
  • .env.example template 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

Additional Resources