Skip to main content

Security Overview

Security Architecture

The Lofty Golden Oil ERP system implements a defense-in-depth strategy with four distinct security layers:

┌─────────────────────────────────────────────┐
│ Network Layer │
│ Oracle Cloud Security Lists / SELinux │
├─────────────────────────────────────────────┤
│ Transport Layer │
│ TLS 1.2+ / Let's Encrypt / HSTS │
├─────────────────────────────────────────────┤
│ Application Layer │
│ JWT Auth / RBAC / Input Validation / CORS │
├─────────────────────────────────────────────┤
│ Data Layer │
│ bcrypt Hashing / Parameterized Queries │
│ Encrypted Secrets / Audit Logging │
└─────────────────────────────────────────────┘

Each layer operates independently. A failure in one layer is mitigated by the layers above and below it.


Authentication System

JWT Bearer Tokens

PropertyValue
AlgorithmHS256 (HMAC-SHA256)
Expiry24 hours
Token StorageClient-side (localStorage or memory)
Refresh TokensNot implemented — re-login required

Flow:

  1. User submits email and password to POST /api/auth/login.
  2. Server validates credentials against users table.
  3. Server generates a signed JWT containing: userId, role, email.
  4. JWT is returned to the client.
  5. Client includes the JWT in the Authorization: Bearer <token> header on all subsequent requests.
  6. Server middleware validates the JWT signature and expiry on every protected route.

Security Considerations:

  • The JWT secret must be stored in an environment variable, never hardcoded.
  • The secret should be a cryptographically strong random string (minimum 256 bits).
  • The secret should be rotated periodically; upon rotation, all active tokens are invalidated.
  • No refresh token mechanism exists — once expired, users must re-authenticate.

Password Hashing

PropertyValue
Algorithmbcrypt (via bcryptjs)
Salt Rounds12 (default)
Hash Length60 characters

Implementation:

  • Passwords are never stored in plaintext.
  • Every password is hashed with a unique salt before storage.
  • Verification compares the provided password against the stored hash using bcrypt.compare().
  • The password_hash column is VARCHAR(255) to accommodate future algorithm changes.

Password Policy Recommendations:

  • Minimum 8 characters
  • At least one uppercase letter, one number, and one special character
  • No reuse of last 5 passwords
  • Account lockout after 5 consecutive failed attempts (recommended — not yet implemented)

Authorization

Role-Based Access Control (RBAC)

Four roles are defined in the system:

RoleDescription
adminFull system access — all modules, user management, settings, audit logs
productionInventory and production management, read-only for customers/suppliers
salesCustomer management, sales, invoicing, payments, read-only for inventory/suppliers
accountantExpense management, full accounting module (GL, vouchers, journals, reports)

Enforcement is two-tiered:

  1. Server-side middleware: Each API route has role-based middleware that rejects unauthorized requests with HTTP 403. This is the authoritative access control mechanism.
  2. Client-side rendering: React components conditionally render navigation and buttons based on the user's role. This is UX-only — it provides a clean interface but is not a security mechanism. All security enforcement happens server-side.

Transport Security

TLS 1.2+ via Let's Encrypt

PropertyValue
Certificate AuthorityLet's Encrypt (via certbot)
Domainloftygoldenoil.com
Auto-RenewalYes (certbot cron job)
ProtocolTLS 1.2 and 1.3
CiphersModern cipher suites only

HSTS (HTTP Strict Transport Security)

Nginx is configured to send HSTS headers, instructing browsers to only communicate with the server over HTTPS:

Strict-Transport-Security: max-age=31536000; includeSubDomains

Nginx SSL Configuration

server {
listen 443 ssl http2;
server_name loftygoldenoil.com;

ssl_certificate /etc/letsencrypt/live/loftygoldenoil.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/loftygoldenoil.com/privkey.pem;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
}

Network Security

Oracle Cloud Security Lists

DirectionProtocolPortSource/Destination
InboundTCP22Source: All (SSH access)
InboundTCP80Source: All (HTTP)
InboundTCP443Source: All (HTTPS)
InboundTCP8443Source: All (Staging)
OutboundAllAllDestination: All

Hardening Recommendations:

  • Restrict SSH (port 22) access to known IP addresses only.
  • Consider adding a VPN or bastion host for SSH access.
  • Only ports 80, 443, and 8443 are accessible from the internet — Node.js (port 4000) is not directly exposed.

SELinux

Security-Enhanced Linux is enforcing on the Oracle Linux 9 host with the following boolean permissions:

BooleanValuePurpose
httpd_can_network_connectonAllows Nginx to make outbound network connections (e.g., for API proxying)
httpd_can_network_connect_dbonAllows Nginx to connect to database ports

Application Security

CORS Configuration

app.use(cors()); // Currently fully open

Current State: CORS is configured to accept requests from any origin. This is acceptable during development but must be restricted in production.

Recommended Production Configuration:

app.use(cors({
origin: [
'https://loftygoldenoil.com',
'http://170.9.14.60:8443'
],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));

Body Size Limit

PropertyValue
Maximum Request Body15 MB
Configured InExpress middleware (express.json(&#123; limit: '15mb' &#125;))

Prevents denial-of-service attacks via oversized payloads.

SQL Injection Prevention

All database queries use parameterized queries (prepared statements) via the MySQL2 library. User input is never concatenated directly into SQL strings.

// Safe — parameterized query
const [rows] = await pool.execute(
'SELECT * FROM users WHERE email = ? AND status = ?',
[email, 'active']
);

// Unsafe — NEVER done in this codebase
const query = `SELECT * FROM users WHERE email = '${email}'`;

XSS Prevention

  • React automatically escapes all rendered values, preventing XSS in the frontend.
  • API responses return Content-Type: application/json — browsers do not execute JSON as HTML.
  • No user-generated HTML is rendered without sanitization.

Data Security

Password Storage

  • Passwords are hashed using bcrypt with 12 salt rounds before storage.
  • The password_hash column is never exposed in API responses.
  • The login endpoint does not reveal whether an email exists in the system (returns a generic error message).

Secret Management

SecretStorage Method
Database passwordEnvironment variable
JWT signing secretEnvironment variable
Resend API keyEnvironment variable
SSH private keyFilesystem (restricted permissions)
PM2 ecosystem configFilesystem (restricted permissions)

Rules:

  • Secrets are never committed to source control.
  • Environment variables are used for all sensitive configuration.
  • The .env file is listed in .gitignore.
  • Deploy keys are stored in deploy/ directory with chmod 600 permissions.

Database Credentials

PropertyValue
Usernamelgo
PasswordStored in environment variable (not in code)
Hostlocalhost (database only accessible from the VPS itself)
PermissionsFull access to lgo_erp_prod and lgo_erp_staging only

Infrastructure Security

SSH Key Authentication

PropertyValue
Authentication MethodSSH key only
Password AuthenticationDisabled
Key TypeRSA or ED25519
Key Location/Users/fuhsi/Documents/helpdesk/lofty-golden-oil-erp/ssh-key-2026-07-25.key
Deploy Keydeploy/oracle_key (private), deploy/vps_ssh_key.txt (copy)
VPS Useropc

Rules:

  • SSH private keys must have chmod 600 permissions.
  • Never share or transmit private keys over insecure channels.
  • Rotate SSH keys periodically.

PM2 Process Isolation

PropertyValue
Config Path/opt/lgo-erp/ecosystem.config.cjs
Production Processlgo-prod (port 4000, database lgo_erp_prod)
Staging Processlgo-staging (port 8443, database lgo_erp_staging)
Run Modefork (single instance per environment)
Restart PolicyAutomatic restart on crash

PM2 runs each environment as a separate OS-level process with its own memory space, preventing one environment from affecting the other.

Nginx as Reverse Proxy

Internet → Nginx (443) → Node.js (4000)
  • Node.js runs on port 4000 and is not directly accessible from the internet.
  • Nginx handles SSL termination, static file serving, and request proxying.
  • Nginx serves the React frontend as static files from /opt/lgo-erp/dist/.
  • API requests (/api/*) are proxied to Node.js on port 4000.
  • SPA fallback via try_files $uri /index.html ensures client-side routing works.

Audit Trail

audit_log Table

Every significant system action is recorded in the audit_log table:

ColumnPurpose
user_idWhich user performed the action
actionWhat was done (CREATE, UPDATE, DELETE, LOGIN, APPROVE, REJECT)
entity_typeWhich entity was affected (sale, invoice, expense, etc.)
entity_idID of the specific record
detailsHuman-readable description of the change
ip_addressClient IP address (supports IPv6)
created_atTimestamp of the action

Key Properties:

  • Audit logs are append-only — they are never deleted or modified.
  • The audit_log table has no DELETE endpoint.
  • All CRUD operations, login events, and approval actions are logged.
  • IP addresses are captured for security forensics.

Security Best Practices

For System Administrators

PracticeFrequency
Review audit logs for anomaliesDaily
Verify SSL certificate renewalMonthly
Check for OS security updatesWeekly
Rotate SSH keysQuarterly
Review user access and remove inactive accountsMonthly
Backup database and verify restorationDaily (automated) + Monthly (manual test)
Rotate JWT secretQuarterly
Review Nginx access logs for suspicious activityWeekly

For Developers

PracticeDescription
Never commit secretsUse environment variables for all sensitive data
Use parameterized queriesAlways use prepared statements — never concatenate user input into SQL
Validate all inputValidate and sanitize all user input on the server side
Use HTTPSAll communication must be encrypted in transit
Follow the principle of least privilegeGrant only the minimum permissions needed
Keep dependencies updatedRun npm audit regularly and update vulnerable packages
Review error messagesEnsure error messages do not leak sensitive information

For End Users

PracticeDescription
Use strong passwordsMinimum 8 characters, mix of letters, numbers, and symbols
Do not share credentialsEach user should have their own account
Log out when finishedEspecially on shared computers
Report suspicious activityNotify IT immediately if something seems wrong

Known Security Considerations

Current Gaps

IssueSeverityRecommendation
CORS is fully openHighRestrict to production and staging domains
No rate limitingHighImplement rate limiting on auth endpoints and API
No account lockoutMediumAdd lockout after failed login attempts
SSH port 22 open to allMediumRestrict to known IP addresses
No refresh token rotationMediumImplement refresh tokens for better session management
No Content Security PolicyMediumAdd CSP headers via Nginx
No HTTP security headersLowAdd X-Frame-Options, X-Content-Type-Options, etc.
No input length validationLowAdd server-side validation for field lengths

Remediation Priority

  1. Immediate: Restrict CORS origins
  2. Within 1 week: Implement rate limiting on login and API endpoints
  3. Within 1 month: Add HTTP security headers, CSP, account lockout
  4. Within 3 months: Implement refresh tokens, restrict SSH access

Compliance Considerations

Nigerian Data Protection Regulation (NDPR) 2019

  • Personal data of customers, suppliers, and employees must be collected and processed lawfully.
  • Data subjects have the right to access, correct, and request deletion of their data.
  • A Data Protection Officer should be appointed if processing personal data at scale.
  • Cross-border data transfers must comply with NDPR requirements.

PCI-DSS Awareness

  • The ERP system records payment methods and reference numbers for reconciliation.
  • No credit card numbers or payment credentials are stored in the system.
  • All payment processing occurs through external providers (bank transfers, cash).
  • If payment card processing is added in the future, PCI-DSS compliance must be assessed.

Financial Record Retention

  • Nigerian tax law requires retention of financial records for a minimum of 7 years.
  • The audit log should be retained indefinitely for compliance and forensics.
  • Database backups should be retained for at least 12 months.

Data Protection Principles

PrincipleImplementation
LawfulnessData collected for legitimate business purposes
Purpose LimitationData used only for ERP business functions
Data MinimizationOnly necessary data is collected
AccuracyUsers can update their own records; admin manages others
Storage LimitationRetention policies defined per data category
Integrity & Confidentialitybcrypt passwords, parameterized queries, audit logging
AccountabilityAudit trail tracks all data access and modifications