Threat Modeling12 min read

Attack Surface Reduction: Minimizing Your Security Exposure

Systematic approaches to identifying, mapping, and reducing your organization's attack surface through architecture decisions, security controls, and continuous monitoring.

AS

Threat Modeling Team

Security Architecture

Introduction

Your attack surface—the sum of all possible entry points an attacker could exploit—grows with every new feature, service, and integration. Attack surface reduction (ASR) is the practice of systematically minimizing these exposure points while maintaining business functionality. This guide provides practical strategies for identifying and reducing your attack surface.

1. Attack Surface Mapping

Digital Asset Inventory

// Automated asset discovery
interface Asset {
  id: string;
  type: 'api' | 'web-app' | 'mobile-app' | 'service' | 'database' | 'third-party';
  name: string;
  url?: string;
  exposureLevel: 'public' | 'authenticated' | 'internal' | 'private';
  dataClassification: 'public' | 'internal' | 'confidential' | 'sensitive';
  authenticationMethods: string[];
  dependencies: string[];
  owner: string;
  riskScore: number;
}

class AttackSurfaceMapper {
  async discoverAssets(): Promise<Asset[]> {
    const assets = [];
    
    // Discover from infrastructure
    const cloudAssets = await this.scanCloudInfrastructure();
    const dnsRecords = await this.enumerateDNS();
    const apiEndpoints = await this.discoverAPIs();
    
    // Discover from code
    const codeAssets = await this.scanCodebase();
    
    // Discover from network
    const networkAssets = await this.scanNetwork();
    
    // Discover third-party integrations
    const thirdParty = await this.identifyThirdPartyServices();
    
    return [...cloudAssets, ...dnsRecords, ...apiEndpoints, 
            ...codeAssets, ...networkAssets, ...thirdParty];
  }
  
  async scanCloudInfrastructure(): Promise<Asset[]> {
    const ec2Instances = await aws.ec2.describeInstances();
    const loadBalancers = await aws.elb.describeLoadBalancers();
    const apigateways = await aws.apigateway.getRestApis();
    
    return [
      ...this.mapEC2ToAssets(ec2Instances),
      ...this.mapELBToAssets(loadBalancers),
      ...this.mapAPIGatewayToAssets(apigateways)
    ];
  }
  
  calculateRiskScore(asset: Asset): number {
    let score = 0;
    
    // Exposure level
    const exposureScores = { public: 10, authenticated: 7, internal: 4, private: 1 };
    score += exposureScores[asset.exposureLevel];
    
    // Data sensitivity
    const dataScores = { sensitive: 10, confidential: 7, internal: 4, public: 1 };
    score += dataScores[asset.dataClassification];
    
    // Authentication strength
    if (!asset.authenticationMethods.includes('mfa')) score += 5;
    if (asset.authenticationMethods.includes('none')) score += 10;
    
    // Dependencies (complexity)
    score += Math.min(asset.dependencies.length, 10);
    
    return score;
  }
}

// Export attack surface map
const attackSurface = await mapper.discoverAssets();
console.log(`Total attack surface: ${attackSurface.length} assets`);
console.log(`High-risk assets: ${attackSurface.filter(a => a.riskScore > 20).length}`);

Visual Attack Surface Map

// Generate attack surface diagram
class AttackSurfaceVisualizer {
  generateMermaidDiagram(assets: Asset[]): string {
    const publicAssets = assets.filter(a => a.exposureLevel === 'public');
    const internalAssets = assets.filter(a => a.exposureLevel === 'internal');
    
    return `
graph TD
    Internet[Internet/Attacker]
    
    subgraph "Public Attack Surface"
      ${publicAssets.map(a => `${a.id}[${a.name}]`).join('\n      ')}
    end
    
    subgraph "Internal Systems"
      ${internalAssets.map(a => `${a.id}[${a.name}]`).join('\n      ')}
    end
    
    Internet --> API[Public API]
    Internet --> WebApp[Web Application]
    API --> AuthService[Auth Service]
    WebApp --> AuthService
    AuthService --> Database[(User Database)]
    `;
  }
}

2. Network Attack Surface Reduction

Minimize Public Exposure

// Infrastructure as Code - Minimal exposure
// Terraform example
resource "aws_security_group" "app" {
  name = "app-sg"
  
  # Only allow HTTPS from internet
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  # No direct SSH access - use SSM Session Manager instead
  # ingress {
  #   from_port   = 22
  #   to_port     = 22
  #   protocol    = "tcp"
  #   cidr_blocks = ["0.0.0.0/0"]  # DON'T DO THIS
  # }
  
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

// Database in private subnet - NO public access
resource "aws_db_instance" "main" {
  identifier              = "app-db"
  engine                  = "postgres"
  instance_class          = "db.t3.micro"
  publicly_accessible     = false  # CRITICAL
  db_subnet_group_name    = aws_db_subnet_group.private.name
  vpc_security_group_ids  = [aws_security_group.database.id]
}

resource "aws_security_group" "database" {
  name = "database-sg"
  
  # Only allow from application tier
  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }
}

Network Segmentation

// Multi-tier architecture with network isolation
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

// Public subnet - Only load balancers
resource "aws_subnet" "public" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.1.0/24"
  map_public_ip_on_launch = false
}

// Private subnet - Application servers
resource "aws_subnet" "private_app" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.10.0/24"
}

// Isolated subnet - Databases
resource "aws_subnet" "private_data" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.20.0/24"
}

// Network ACLs for defense in depth
resource "aws_network_acl" "data_tier" {
  vpc_id     = aws_vpc.main.id
  subnet_ids = [aws_subnet.private_data.id]
  
  # Only allow from application tier
  ingress {
    rule_no    = 100
    protocol   = "tcp"
    from_port  = 5432
    to_port    = 5432
    cidr_block = aws_subnet.private_app.cidr_block
    action     = "allow"
  }
  
  # Deny all other inbound
  ingress {
    rule_no    = 200
    protocol   = "-1"
    cidr_block = "0.0.0.0/0"
    action     = "deny"
  }
}

3. API Attack Surface Reduction

Minimize Exposed Endpoints

// API Design - Principle of least exposure
// Bad: Exposing internal implementation
app.get('/api/users/:id', getUser);
app.put('/api/users/:id', updateUser);
app.delete('/api/users/:id', deleteUser);
app.get('/api/users/:id/orders', getUserOrders);
app.get('/api/users/:id/payments', getUserPayments);
app.get('/api/users/:id/internal-metrics', getMetrics);  // EXPOSED!
app.post('/api/users/:id/reset-password-admin', adminResetPassword);  // EXPOSED!

// Good: Minimal, purpose-driven API
app.get('/api/me', getCurrentUser);              // Self-service only
app.patch('/api/me', updateCurrentUser);
app.get('/api/me/orders', getCurrentUserOrders);
// Admin functions on separate, heavily protected endpoint
app.post('/api/admin/users/:id/reset-password', 
  requireRole('admin'), 
  requireMFA, 
  rateLimitStrict,
  adminResetPassword
);

// API Gateway configuration
const apiConfig = {
  rateLimiting: {
    requestsPerSecond: 100,
    burstSize: 200
  },
  authentication: 'required',
  cors: {
    allowedOrigins: ['https://app.company.com'],  // Not '*'
    allowedMethods: ['GET', 'POST', 'PATCH']       // Not all methods
  },
  disabledEndpoints: [
    '/api/debug',
    '/api/internal',
    '/api/admin'  // Separate admin API gateway
  ]
};

API Versioning and Deprecation

// Systematically deprecate and remove old API versions
class APILifecycleManager {
  async deprecateVersion(version: string, sunsetDate: Date): Promise<void> {
    // Add deprecation headers
    app.use(`/api/${version}/*`, (req, res, next) => {
      res.set('Sunset', sunsetDate.toUTCString());
      res.set('Deprecation', 'true');
      res.set('Link', '<https://docs.api.com/migration>; rel="alternate"');
      next();
    });
    
    // Log usage for migration planning
    await this.trackDeprecatedAPIUsage(version);
    
    // Notify clients
    await this.notifyAPIClients(version, sunsetDate);
  }
  
  async removeVersion(version: string): Promise<void> {
    // Remove routes
    app._router.stack = app._router.stack.filter(
      route => !route.path?.startsWith(`/api/${version}`)
    );
    
    // Log removal
    logger.info(`Removed API version ${version}`);
    
    // Update attack surface inventory
    await attackSurfaceMapper.removeAsset(`api-${version}`);
  }
}

// Scheduled API cleanup
// Remove v1 after 1 year deprecation period
setTimeout(() => {
  apiManager.removeVersion('v1');
}, 365 * 24 * 60 * 60 * 1000);

4. Authentication Attack Surface

Reduce Authentication Methods

// Consolidate authentication methods
// Bad: Multiple auth methods increase attack surface
const authMethods = {
  usernamePassword: true,
  apiKey: true,
  basicAuth: true,
  oauth: true,
  saml: true,
  customToken: true  // Custom implementation = more vulnerabilities
};

// Good: Standardized, well-tested auth
const authMethods = {
  oauth2: {
    provider: 'Auth0',  // Managed service
    flows: ['authorization-code-pkce'],  // Only secure flow
    mfa: 'required'
  },
  apiKey: {
    enabled: false  // Deprecated in favor of OAuth
  }
};

// Implement single sign-on
class SingleSignOn {
  async authenticate(req: Request): Promise<User> {
    // Only one authentication path
    const token = this.extractBearerToken(req);
    const payload = await this.verifyJWT(token);
    
    // All authentication goes through same validation
    await this.validateMFA(payload);
    await this.checkRevokedTokens(payload.jti);
    await this.enforceSessionLimits(payload.sub);
    
    return this.loadUser(payload.sub);
  }
}

5. Third-Party Integration Surface

Vendor Risk Assessment

// Track and minimize third-party integrations
interface ThirdPartyIntegration {
  vendor: string;
  purpose: string;
  dataShared: string[];
  accessLevel: 'read' | 'write' | 'admin';
  integrationMethod: 'api' | 'sdk' | 'embed' | 'direct-db';
  riskScore: number;
  lastReviewed: Date;
  alternatives: string[];
}

const integrations: ThirdPartyIntegration[] = [
  {
    vendor: 'SendGrid',
    purpose: 'Transactional emails',
    dataShared: ['email', 'name'],
    accessLevel: 'read',
    integrationMethod: 'api',
    riskScore: 3,
    lastReviewed: new Date('2026-01-15'),
    alternatives: ['AWS SES', 'Self-hosted']
  },
  {
    vendor: 'Analytics Provider',
    purpose: 'User tracking',
    dataShared: ['userId', 'pageViews', 'events'],
    accessLevel: 'write',
    integrationMethod: 'sdk',
    riskScore: 7,  // High risk - lots of data
    lastReviewed: new Date('2025-12-01'),
    alternatives: ['Self-hosted Matomo', 'Remove entirely']
  }
];

// Quarterly review process
class ThirdPartyReview {
  async reviewIntegrations(): Promise<void> {
    for (const integration of integrations) {
      if (this.shouldReview(integration)) {
        await this.assessAlternatives(integration);
        await this.minimizeDataSharing(integration);
        await this.reviewAccessLevel(integration);
      }
    }
  }
  
  async minimizeDataSharing(integration: ThirdPartyIntegration): Promise<void> {
    // Can we share less data?
    const minimized = integration.dataShared.filter(field => 
      this.isAbsolutelyNecessary(integration.purpose, field)
    );
    
    if (minimized.length < integration.dataShared.length) {
      logger.info(`Reduced data sharing with ${integration.vendor}`);
      integration.dataShared = minimized;
      await this.updateIntegration(integration);
    }
  }
}

6. Container and Service Surface

Minimize Running Services

// Dockerfile - Minimal attack surface
# Bad: Large base with many unnecessary services
FROM ubuntu:latest
RUN apt-get update && apt-get install -y \
    openssh-server \  # Why SSH in container?
    telnet \           # Ancient, insecure
    curl wget \
    vim nano \
    net-tools

# Good: Distroless image with only application
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=build /app/dist /app
WORKDIR /app
CMD ["server.js"]

# Scan for unnecessary ports/services
docker run --rm -it alpine sh
# netstat -tulpn
# Only the application port should be listening

Service Mesh Security

// Istio configuration - Zero trust between services
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT  # Require mTLS for all service-to-service

---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: frontend-to-backend
  namespace: production
spec:
  selector:
    matchLabels:
      app: backend
  action: ALLOW
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/production/sa/frontend"]
    to:
    - operation:
        methods: ["GET", "POST"]
        paths: ["/api/*"]
    # Backend cannot be accessed from internet directly

7. Continuous Attack Surface Monitoring

Automated Surface Scanning

// Daily attack surface monitoring
class AttackSurfaceMonitor {
  async dailyScan(): Promise<void> {
    // Check for new public-facing assets
    const currentAssets = await this.discoverPublicAssets();
    const previousAssets = await this.loadPreviousScan();
    
    const newAssets = currentAssets.filter(
      asset => !previousAssets.find(p => p.id === asset.id)
    );
    
    if (newAssets.length > 0) {
      await this.alertSecurityTeam({
        type: 'NEW_ATTACK_SURFACE',
        assets: newAssets,
        message: `${newAssets.length} new public assets detected`
      });
    }
    
    // Check for insecure configurations
    await this.scanForMisconfigurations(currentAssets);
    
    // Check for expiring certificates
    await this.checkCertificateExpiry(currentAssets);
    
    // Update inventory
    await this.saveCurrentScan(currentAssets);
  }
  
  async scanForMisconfigurations(assets: Asset[]): Promise<void> {
    for (const asset of assets) {
      // Check for missing security headers
      if (asset.type === 'web-app') {
        const headers = await this.checkSecurityHeaders(asset.url);
        if (!headers.hasCSP || !headers.hasHSTS) {
          await this.createRemediationTicket(asset, 'Missing security headers');
        }
      }
      
      // Check for default credentials
      if (asset.authenticationMethods.includes('basic-auth')) {
        const hasDefaults = await this.testDefaultCredentials(asset);
        if (hasDefaults) {
          await this.alertCritical(asset, 'DEFAULT CREDENTIALS DETECTED');
        }
      }
    }
  }
}

// GitHub Action for continuous monitoring
name: Attack Surface Monitoring
on:
  schedule:
    - cron: '0 */6 * * *'  # Every 6 hours
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - name: Scan public endpoints
        run: |
          nmap -p 80,443,8080 company.com
          nuclei -u https://company.com -severity high,critical
          
      - name: Check subdomain takeover
        run: |
          subjack -w subdomains.txt -t 100 -timeout 30 -o results.txt
          
      - name: Alert on new findings
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK }}

Attack Surface Reduction Checklist

Key ASR Strategies:

  • ✅ Maintain complete asset inventory
  • ✅ Minimize public-facing services
  • ✅ Use network segmentation (DMZ, private subnets)
  • ✅ Close unnecessary ports and services
  • ✅ Consolidate authentication methods
  • ✅ Remove deprecated API versions
  • ✅ Minimize third-party integrations
  • ✅ Use minimal container images
  • ✅ Implement zero-trust architecture
  • ✅ Regular attack surface scanning
  • ✅ Automate surface change detection
  • ✅ Review and remove unused features
  • ✅ Disable administrative interfaces
  • ✅ Use VPN/bastion for internal access
  • ✅ Document and justify every public endpoint

Conclusion

Attack surface reduction is an ongoing discipline that requires continuous attention. Every new feature, integration, or infrastructure change potentially expands your attack surface. By systematically mapping your assets, minimizing unnecessary exposure, and continuously monitoring for changes, you significantly reduce the opportunities available to attackers. Remember: the best vulnerability to patch is the one that never gets exposed in the first place.