Skip to main content

Overview

RDSWeb Custom handles authentication and application access for your Remote Desktop Services infrastructure. Proper security configuration is critical to protect your environment. This guide covers:
  • JWT token security
  • HTTPS/TLS configuration
  • CORS policies
  • Active Directory service account security
  • Network security

JWT Authentication

Secret Configuration

The JWT secret is used to sign authentication tokens. Configure a strong secret in .env:
JWT_SECRET=your_very_long_random_secret_at_least_64_characters_long
JWT_EXPIRES_IN=8h
Default Secret: The default value dev_secret_insecure_change_in_production is insecure and must be changed before production deployment.See backend/src/config.js:8:
secret: process.env.JWT_SECRET || 'dev_secret_insecure_change_in_production',

Generating a Secure Secret

Use one of these methods:
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

Token Expiration

Configure token lifetime based on your security requirements:
EnvironmentRecommended Expiration
Development12h
Production (internal)8h
Production (public)1h
High-security30m
JWT_EXPIRES_IN=8h  # 8 hours

Token Storage

The application uses HTTP-only cookies for token storage:
  • XSS Protection: JavaScript cannot access HTTP-only cookies
  • CSRF Mitigation: Combined with SameSite attribute
  • Automatic Transmission: Browser handles token submission
See backend/src/routes/auth.js for implementation details.

HTTPS/TLS Configuration

Production Requirement: Always use HTTPS in production. Never transmit credentials over unencrypted HTTP.

Reverse Proxy with TLS

The recommended deployment uses a reverse proxy (IIS, Nginx, or Apache) to handle TLS:
<!-- web.config for IIS reverse proxy -->
<system.webServer>
  <rewrite>
    <rules>
      <rule name="RDWeb Backend" stopProcessing="true">
        <match url="(.*)" />
        <action type="Rewrite" url="http://localhost:3000/{R:1}" />
      </rule>
    </rules>
  </rewrite>
  <httpProtocol>
    <customHeaders>
      <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains" />
    </customHeaders>
  </httpProtocol>
</system.webServer>

Certificate Requirements

  • Use certificates from a trusted CA (not self-signed)
  • Ensure the certificate covers the hostname users access
  • Set certificate expiration monitoring
  • Use at least 2048-bit RSA or 256-bit ECC keys

CORS Configuration

The backend restricts Cross-Origin Resource Sharing to trusted origins (see backend/src/index.js:19-26):
app.use(
    cors({
        origin: ['http://localhost:4200', 'http://localhost:4300'],
        credentials: true,
        methods: ['GET', 'POST', 'OPTIONS'],
        allowedHeaders: ['Content-Type', 'Authorization'],
    })
);

Production CORS Configuration

Update the origin array for production:
const allowedOrigins = process.env.CORS_ORIGINS 
    ? process.env.CORS_ORIGINS.split(',') 
    : ['https://rdweb.contoso.local'];

app.use(
    cors({
        origin: allowedOrigins,
        credentials: true,
        methods: ['GET', 'POST', 'OPTIONS'],
        allowedHeaders: ['Content-Type', 'Authorization'],
    })
);
.env configuration:
CORS_ORIGINS=https://rdweb.contoso.local,https://rdweb-backup.contoso.local
Never use wildcard origins (*) in production when credentials: true. This is a security risk.

Security Headers (Helmet)

The application uses Helmet for security headers (see backend/src/index.js:16):
app.use(helmet());
Helmet automatically sets:
  • Strict-Transport-Security - Enforces HTTPS
  • X-Content-Type-Options: nosniff - Prevents MIME type sniffing
  • X-Frame-Options: SAMEORIGIN - Prevents clickjacking
  • X-XSS-Protection: 1; mode=block - Enables XSS filter
  • Removes X-Powered-By header

Custom Helmet Configuration

For advanced scenarios, customize Helmet:
app.use(helmet({
    contentSecurityPolicy: {
        directives: {
            defaultSrc: ["'self'"],
            styleSrc: ["'self'", "'unsafe-inline'"],
            scriptSrc: ["'self'"],
            imgSrc: ["'self'", "data:"],
        },
    },
    hsts: {
        maxAge: 31536000,
        includeSubDomains: true,
        preload: true,
    },
}));

Active Directory Service Account

Principle of Least Privilege

The AD service account should have minimal permissions:

Account Configuration

# Create service account with secure settings
New-ADUser -Name "svc-rdweb" `
    -SamAccountName "svc-rdweb" `
    -UserPrincipalName "[email protected]" `
    -Path "OU=Service Accounts,DC=contoso,DC=local" `
    -AccountPassword (ConvertTo-SecureString "YourSecurePassword!" -AsPlainText -Force) `
    -Enabled $true `
    -PasswordNeverExpires $false `
    -CannotChangePassword $false `
    -Description "RDSWeb Custom LDAP Service Account"

# Deny interactive logon (security best practice)
$user = Get-ADUser svc-rdweb
Set-ADUser $user -Replace @{"msDS-UserAccountDisabled"="True"}

Password Policy

  • Minimum Length: 20 characters
  • Complexity: Uppercase, lowercase, numbers, symbols
  • Rotation: Every 90 days
  • Storage: Use a secrets management system (Azure Key Vault, HashiCorp Vault)
  • Never: Store in source control or share via email

Monitoring

Enable AD audit logging for the service account:
# Enable audit policy for logon events
Auditpol /set /subcategory:"Logon" /success:enable /failure:enable

# Monitor Event IDs:
# 4624 - Successful logon
# 4625 - Failed logon
# 4768 - Kerberos TGT requested

LDAP Security

Use LDAPS (LDAP over TLS)

LDAP_URL=ldaps://dc01.contoso.local:636

Enable Certificate Validation

Edit backend/src/services/adService.js:107 to enable certificate validation:
const adOptions = {
    ldapOpts: {
        url: config.ldap.url,
        tlsOptions: { 
            rejectUnauthorized: true,  // CRITICAL: Set to true in production
            ca: [fs.readFileSync('/etc/ssl/certs/ca-cert.pem')]
        },
    },
    // ...
};
The default setting rejectUnauthorized: false is insecure and should only be used in development.

Network Security

Firewall Rules

Restrict access to the backend server:
PortProtocolSourcePurpose
3000TCPReverse ProxyBackend API (internal only)
443TCPUser NetworkHTTPS (reverse proxy)
389TCPBackend ServerLDAP to Domain Controller
636TCPBackend ServerLDAPS to Domain Controller
135, 445, 49152+TCPBackend ServerWMI to RD Connection Broker

Network Segmentation

Deploy in a segmented architecture:
┌─────────────────┐
│   Users (DMZ)   │
└────────┬────────┘
         │ HTTPS (443)

┌─────────────────┐
│  Reverse Proxy  │
└────────┬────────┘
         │ HTTP (3000, internal)

┌─────────────────┐
│  Backend (Node) │
└────────┬────────┘
         │ LDAPS (636), WMI (135)

┌─────────────────┐
│  AD / RDCB      │
└─────────────────┘

Environment Variables Security

Never Commit .env Files

Add to .gitignore:
.env
.env.local
.env.production
*.env

Use Secrets Management

For production, use a secrets management system:
const { DefaultAzureCredential } = require('@azure/identity');
const { SecretClient } = require('@azure/keyvault-secrets');

const credential = new DefaultAzureCredential();
const client = new SecretClient('https://your-vault.vault.azure.net', credential);

const jwtSecret = await client.getSecret('JWT-SECRET');
const adPassword = await client.getSecret('AD-SERVICE-PASS');

Logging and Monitoring

Enable Audit Logging

Log security-relevant events:
// backend/src/middleware/auditLogger.js
const winston = require('winston');

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
        new winston.transports.File({ filename: 'audit.log' })
    ]
});

function auditLog(event, user, details) {
    logger.info({
        timestamp: new Date().toISOString(),
        event,
        user: user?.username || 'anonymous',
        ip: details.ip,
        userAgent: details.userAgent,
        details
    });
}

module.exports = { auditLog };

Events to Log

  • Authentication attempts (success and failure)
  • Application access requests
  • Configuration changes
  • Service account usage
  • Errors and exceptions

Security Checklist

1

JWT Secret

✅ Generate a secure JWT secret (64+ characters)✅ Store in environment variables or secrets manager✅ Never commit to source control
2

HTTPS/TLS

✅ Enable HTTPS with valid certificates✅ Use TLS 1.2 or higher✅ Enable HSTS headers
3

CORS Configuration

✅ Set specific allowed origins (no wildcards)✅ Update for production domains✅ Enable credentials only for trusted origins
4

AD Service Account

✅ Create dedicated service account (not a user account)✅ Grant read-only permissions✅ Use strong password (20+ characters)✅ Enable password rotation (90 days)✅ Deny interactive logon
5

LDAP Security

✅ Use LDAPS (port 636)✅ Enable certificate validation✅ Install CA certificates on Node.js server
6

Network Security

✅ Configure firewall rules (minimal ports)✅ Segment networks (DMZ, internal)✅ Restrict WMI access to backend server
7

Monitoring

✅ Enable audit logging✅ Monitor authentication failures✅ Set up alerts for suspicious activity
8

Simulation Mode

✅ Disable in production (SIMULATION_MODE=false)✅ Verify with health endpoint

Compliance Considerations

GDPR / Data Protection

  • Log only necessary user data
  • Implement data retention policies
  • Provide user data export capabilities
  • Document data processing activities

Industry Standards

  • NIST Cybersecurity Framework: Implement identity and access management controls
  • CIS Controls: Follow endpoint and network security guidelines
  • ISO 27001: Maintain information security management system

Incident Response

Prepare an incident response plan:
  1. Detection: Monitor logs for suspicious activity
  2. Containment: Disable compromised service accounts
  3. Eradication: Rotate JWT secrets, change passwords
  4. Recovery: Restore from known-good backups
  5. Lessons Learned: Update security controls

Build docs developers (and LLMs) love