CrackedRuby CrackedRuby

Principle of Least Privilege

Overview

The Principle of Least Privilege (PoLP) dictates that entities within a system receive only the permissions necessary to accomplish their specific tasks, no more and no less. This foundational security principle emerged from military access control systems in the 1970s and became formalized in computer security through the work of Jerome Saltzer and Michael Schroeder in their 1975 paper on protection mechanisms.

The principle applies across multiple layers of software systems: operating system processes, database connections, API endpoints, user roles, file system permissions, and network access controls. Each component or actor receives a minimal set of privileges that allows function execution without granting unnecessary capabilities that could be exploited.

PoLP reduces the attack surface of a system by limiting potential damage from compromised components. When a process runs with excessive privileges, successful exploitation grants attackers broader system access. Conversely, properly restricted privileges contain breaches within narrow boundaries.

# Excessive privileges - process runs as root
system("rm -rf /tmp/cache/*")  # Dangerous if exploited

# Least privilege - process runs with specific user permissions
require 'fileutils'
FileUtils.rm_rf(Dir.glob('/tmp/cache/*'), secure: true)

The principle intersects with separation of duties, defense in depth, and the security principle of fail-safe defaults. Systems designed with PoLP naturally compartmentalize functions, making security boundaries explicit and enforceable. This compartmentalization enables fine-grained auditing, since each privilege boundary creates a logging point for monitoring access patterns and detecting anomalies.

Key Principles

The Principle of Least Privilege rests on several interconnected concepts that define its scope and application. These principles establish how permissions should be granted, validated, and constrained throughout a system's lifecycle.

Minimal Permission Sets: Each entity receives exactly the permissions required for its designated operations. This requires identifying the specific resources and operations needed, then granting only those capabilities. Determining minimal permissions involves analyzing the entity's function, dependencies, and data access patterns. Overly broad permissions violate this principle even if they appear convenient during development.

Temporal Restriction: Privileges should be granted for the minimum duration necessary. Rather than permanent elevated access, systems implement time-bound permissions that expire after task completion. This temporal aspect prevents privilege accumulation where entities retain access long after the need disappears.

Contextual Access: Permission grants consider the operational context including time of day, request origin, data sensitivity, and user activity patterns. A database connection might have read-only access during batch processing but require write access during transaction processing. Context-aware restrictions adapt privilege levels to match legitimate usage patterns.

Separation of Privileges: Complex operations decompose into discrete steps, each executed with appropriate privileges. A deployment process might separate code retrieval (low privilege), compilation (medium privilege), and system installation (high privilege). This separation ensures compromise at one stage doesn't automatically grant access to other stages.

# Separation of privileges in file processing
class FileProcessor
  def process(file_path)
    content = read_file(file_path)      # Read-only operation
    result = transform(content)         # No file system access
    write_file(result, file_path)       # Write-only operation
  end

  private

  def read_file(path)
    # Opens file with read-only mode
    File.open(path, 'r', &:read)
  end

  def transform(content)
    # Pure transformation, no I/O
    content.upcase
  end

  def write_file(content, path)
    # Opens file with write-only mode
    File.open(path, 'w') { |f| f.write(content) }
  end
end

Privilege Escalation Controls: Systems must strictly control transitions from lower to higher privilege levels. Escalation requires explicit authorization, authentication, and audit logging. Many security breaches exploit uncontrolled privilege escalation where lower-privilege contexts gain elevated access through vulnerabilities.

Revocation Mechanisms: The ability to revoke privileges proves as important as granting them. Systems need immediate privilege revocation when threats emerge, employees change roles, or credentials become compromised. Revocation must be atomic and complete, leaving no residual access through cached credentials or lingering sessions.

Accountability and Auditing: Each privilege use requires logging and attribution. Audit trails record which entity accessed what resource, when, and with which privileges. This accountability deters misuse and enables forensic analysis after security incidents.

Security Implications

The Principle of Least Privilege directly impacts system security posture by constraining the potential damage from security breaches, insider threats, and software defects. Understanding these implications guides implementation decisions and helps prioritize security investments.

Attack Surface Reduction: Every unnecessary privilege represents a potential attack vector. Systems running with excessive privileges give attackers more capabilities upon successful exploitation. A web application running as root can modify system files, create new user accounts, or install persistent backdoors. The same application running as a restricted user can only access its designated data directories.

# Insecure: Running with unnecessary privileges
class DatabaseMigration
  def execute
    # Running as superuser can modify any database
    connection = PG.connect(dbname: 'production', user: 'postgres')
    connection.exec('DROP DATABASE backup')  # Dangerous capability
  end
end

# Secure: Restricted to necessary operations
class DatabaseMigration
  def execute
    # Running as migration user with limited grants
    connection = PG.connect(dbname: 'production', user: 'migration_user')
    connection.exec('CREATE TABLE users (id serial)')  # Only permitted operations
  end
end

Lateral Movement Prevention: In multi-tier architectures, least privilege limits lateral movement between system components. When an attacker compromises one service, restrictive permissions prevent using that foothold to pivot to other services. Network segmentation combined with application-level privilege restrictions creates defense in depth that contains breaches.

Credential Exposure Risks: Systems storing credentials with excessive scope amplify breach impact. An API key with full account access creates higher risk than multiple purpose-specific keys. When credentials leak through logs, error messages, or configuration files, least privilege minimizes the damage.

# High risk: Single credential with broad access
class APIClient
  def initialize
    @api_key = ENV['ADMIN_API_KEY']  # Full account access
  end

  def fetch_users
    HTTP.auth("Bearer #{@api_key}").get('/users')
  end

  def delete_user(id)
    HTTP.auth("Bearer #{@api_key}").delete("/users/#{id}")
  end
end

# Lower risk: Separate credentials per function
class APIClient
  def fetch_users
    api_key = ENV['READ_ONLY_API_KEY']
    HTTP.auth("Bearer #{api_key}").get('/users')
  end

  def delete_user(id)
    api_key = ENV['WRITE_API_KEY']
    HTTP.auth("Bearer #{api_key}").delete("/users/#{id}")
  end
end

Insider Threat Mitigation: Least privilege restricts damage from malicious insiders or compromised employee accounts. An engineer's credentials should not grant access to production databases, financial records, or customer data unrelated to their work. Role-based access controls (RBAC) implementing least privilege prevent single accounts from accessing all system components.

Compliance Requirements: Regulations like GDPR, HIPAA, and SOC 2 mandate access controls based on least privilege. Audit requirements demand demonstrable evidence that systems restrict access to the minimum necessary. Compliance frameworks require documented processes for privilege grants, periodic access reviews, and automated privilege revocation.

Supply Chain Security: Third-party integrations and dependencies should operate with minimal privileges. A logging library needs append-only file access, not read/write/execute permissions across the file system. Cloud service integrations require specific API scopes rather than administrative access. This restriction limits damage from compromised dependencies.

Data Breach Containment: When data breaches occur, least privilege determines the scope of exposed information. A compromised service with read-only access to a single table limits exposure compared to full database access. Granular permissions at the data layer create containment boundaries that reduce breach severity.

Ruby Implementation

Ruby provides multiple mechanisms for implementing least privilege across different layers of application security. Understanding these capabilities enables developers to build systems with appropriate privilege restrictions.

File System Permissions: Ruby's File class respects and enforces operating system file permissions. Opening files with explicit mode specifications restricts operations to the minimum required.

# Read-only file access
File.open('config.yml', 'r') do |file|
  config = YAML.load(file.read)
end

# Write-only with creation, no read capability
File.open('log.txt', 'a') do |file|
  file.write("#{Time.now}: Event logged\n")
end

# Explicitly restrict permissions on created files
File.open('secrets.txt', 'w', 0600) do |file|
  file.write(secret_data)
end
# File created with permissions 0600 (owner read/write only)

Process User Context: Ruby applications inherit the permissions of the user executing the process. Systems should run application processes as dedicated users with minimal privileges rather than root or administrative accounts.

# Check current process permissions
def check_privileges
  uid = Process.uid
  gid = Process.gid
  euid = Process.euid

  if euid == 0
    raise SecurityError, "Application running as root - privilege escalation risk"
  end

  puts "Running as UID: #{uid}, GID: #{gid}"
end

# Drop privileges after startup (requires root initially)
def drop_privileges(username)
  user_info = Etc.getpwnam(username)
  
  Process::Sys.setgid(user_info.gid)
  Process::Sys.setuid(user_info.uid)
  
  # Verify privileges dropped
  raise SecurityError unless Process.uid == user_info.uid
end

Database Connection Restrictions: Database libraries support connection-level privilege restrictions through user accounts and connection parameters.

# Read-only database connection
class ReportGenerator
  def initialize
    @connection = PG.connect(
      dbname: 'analytics',
      user: 'readonly_user',
      password: ENV['READONLY_PASSWORD']
    )
  end

  def generate_report
    @connection.exec('SELECT * FROM reports')
  end

  # This will fail with permission denied
  def dangerous_operation
    @connection.exec('DELETE FROM reports')
  end
end

# Separate connections for different privilege levels
class UserManager
  def read_connection
    @read_conn ||= PG.connect(
      dbname: 'users',
      user: 'read_user',
      password: ENV['READ_PASSWORD']
    )
  end

  def write_connection
    @write_conn ||= PG.connect(
      dbname: 'users', 
      user: 'write_user',
      password: ENV['WRITE_PASSWORD']
    )
  end

  def list_users
    read_connection.exec('SELECT id, name FROM users')
  end

  def create_user(name)
    write_connection.exec_params(
      'INSERT INTO users (name) VALUES ($1)',
      [name]
    )
  end
end

API Client Scoping: HTTP clients can implement least privilege through scope-limited authentication tokens and request restrictions.

class GitHubClient
  # Separate clients with different token scopes
  def self.read_client
    @read_client ||= Octokit::Client.new(
      access_token: ENV['GITHUB_READ_TOKEN']  # repo:status, public_repo
    )
  end

  def self.write_client
    @write_client ||= Octokit::Client.new(
      access_token: ENV['GITHUB_WRITE_TOKEN']  # repo
    )
  end

  def list_repositories
    self.class.read_client.repositories
  end

  def create_repository(name)
    self.class.write_client.create_repository(name)
  end
end

Capability-Based Security: Ruby objects can implement capability patterns where references themselves represent authority.

# Capability object that grants specific operations
class FileCapability
  def initialize(path, operations)
    @path = path
    @operations = operations
  end

  def read
    raise SecurityError unless @operations.include?(:read)
    File.read(@path)
  end

  def write(content)
    raise SecurityError unless @operations.include?(:write)
    File.write(@path, content)
  end

  def delete
    raise SecurityError unless @operations.include?(:delete)
    File.delete(@path)
  end
end

# Grant minimum capabilities
def process_file
  capability = FileCapability.new('/data/file.txt', [:read])
  content = capability.read
  # capability.write('new') would raise SecurityError
  content
end

Sandboxing with $SAFE: Older Ruby versions provided $SAFE levels for sandboxing untrusted code. Modern Ruby removed this feature, but the concept illustrates privilege restriction for code execution.

# Modern alternative: Restrict available methods
class SandboxedExecutor
  ALLOWED_METHODS = [:+, :-, :*, :/, :to_s, :to_i].freeze

  def execute(code, context)
    context.instance_eval do
      def method_missing(method, *args)
        unless ALLOWED_METHODS.include?(method)
          raise SecurityError, "Method #{method} not allowed"
        end
        super
      end

      eval(code)
    end
  end
end

executor = SandboxedExecutor.new
executor.execute("2 + 2", binding)  # Works
executor.execute("system('ls')", binding)  # Raises SecurityError

Environment Variable Restrictions: Limit access to sensitive environment variables through wrapper classes.

class RestrictedEnv
  ALLOWED_VARS = ['APP_NAME', 'LOG_LEVEL', 'PORT'].freeze

  def self.[](key)
    unless ALLOWED_VARS.include?(key)
      raise SecurityError, "Access to #{key} not permitted"
    end
    ENV[key]
  end
end

# Use restricted environment
app_name = RestrictedEnv['APP_NAME']  # Works
secret = RestrictedEnv['API_SECRET']   # Raises SecurityError

Design Considerations

Implementing least privilege requires balancing security requirements with operational needs, development velocity, and system complexity. Design decisions affect both security posture and maintainability.

Granularity Trade-offs: Fine-grained permissions provide better security isolation but increase management overhead. A system with hundreds of specific permissions requires more complex authorization logic, increases configuration surface area, and complicates debugging. Coarse-grained permissions simplify management but grant broader access than strictly necessary.

# Fine-grained approach
class PermissionManager
  PERMISSIONS = {
    'user.read.own' => 'Read own user data',
    'user.read.team' => 'Read team user data',
    'user.read.all' => 'Read all user data',
    'user.update.own' => 'Update own user data',
    'user.update.team' => 'Update team user data',
    'user.update.all' => 'Update all user data',
    'user.delete.own' => 'Delete own user data',
    'user.delete.all' => 'Delete all user data'
  }

  def authorize(user, permission)
    user.permissions.include?(permission)
  end
end

# Coarse-grained approach
class SimplePermissionManager
  ROLES = {
    'viewer' => ['read'],
    'editor' => ['read', 'write'],
    'admin' => ['read', 'write', 'delete']
  }

  def authorize(user, action)
    ROLES[user.role]&.include?(action)
  end
end

The optimal granularity depends on data sensitivity, regulatory requirements, and organizational structure. Financial systems handling monetary transactions require fine-grained controls, while internal tools may function adequately with role-based permissions.

Performance Implications: Authorization checks add latency to request processing. Systems performing authorization queries on every operation may experience performance degradation. Caching permission grants reduces overhead but introduces consistency challenges when permissions change.

# Authorization with caching
class CachedAuthorization
  def initialize
    @cache = {}
    @ttl = 300  # 5 minute cache
  end

  def authorize(user_id, resource_id, action)
    cache_key = "#{user_id}:#{resource_id}:#{action}"
    
    if cached = @cache[cache_key]
      return cached[:result] if Time.now - cached[:timestamp] < @ttl
    end

    result = perform_authorization_check(user_id, resource_id, action)
    @cache[cache_key] = { result: result, timestamp: Time.now }
    result
  end

  private

  def perform_authorization_check(user_id, resource_id, action)
    # Expensive database query
    DB[:permissions].where(
      user_id: user_id,
      resource_id: resource_id,
      action: action
    ).any?
  end
end

Privilege Escalation Workflows: Some operations legitimately require elevated privileges. Design patterns for temporary escalation include sudo-style authentication, approval workflows, and time-limited tokens. Each pattern involves trade-offs between security and usability.

# Temporary privilege escalation with approval
class PrivilegeEscalation
  def request_elevated_access(user, operation, duration)
    request = EscalationRequest.create(
      user: user,
      operation: operation,
      expires_at: Time.now + duration,
      status: 'pending'
    )

    notify_approvers(request)
    request
  end

  def execute_with_elevation(request, &block)
    unless request.approved? && request.expires_at > Time.now
      raise SecurityError, "Elevation not authorized"
    end

    # Execute with elevated context
    with_elevated_privileges(request.user) do
      block.call
    end
  ensure
    log_elevated_action(request)
  end

  private

  def with_elevated_privileges(user)
    original_privileges = user.privileges
    user.privileges = :elevated
    yield
  ensure
    user.privileges = original_privileges
  end
end

Default Deny vs Default Allow: Security policies default to denying access unless explicitly granted, or default to allowing access unless explicitly denied. Default deny aligns with least privilege but requires enumerating all legitimate access patterns. Default allow simplifies initial development but creates security gaps.

Separation of Concerns: Privilege logic should separate from business logic. Mixing authorization checks throughout application code creates maintenance burden and increases the likelihood of security gaps. Centralized authorization systems provide consistency and auditability.

# Mixed concerns (problematic)
class UserController
  def update
    user = User.find(params[:id])
    
    # Authorization mixed with business logic
    if current_user.admin? || current_user.id == user.id
      user.update(user_params)
    else
      raise Unauthorized
    end
  end
end

# Separated concerns (preferred)
class UserController
  before_action :authorize_update

  def update
    user = User.find(params[:id])
    user.update(user_params)
  end

  private

  def authorize_update
    authorize User.find(params[:id]), :update?
  end
end

class UserPolicy
  def update?
    user.admin? || user.id == record.id
  end
end

Dynamic vs Static Privileges: Static privilege assignments define access at deployment time through configuration. Dynamic privileges compute access based on runtime state like data attributes, user relationships, or temporal conditions. Dynamic privileges provide flexibility but increase complexity.

Common Pitfalls

Developers frequently make mistakes when implementing least privilege, often due to convenience, misunderstanding, or incomplete security awareness. Recognizing these patterns helps avoid security vulnerabilities.

Privilege Creep: Permissions accumulate over time as features are added without removing obsolete access grants. A service initially needing database read access might later require write access for a new feature. Developers add write permission but forget to restrict it to specific operations, leaving the service with broader access than needed.

# Before: Read-only access
class ReportService
  def initialize
    @db = Database.connect(user: 'readonly')
  end
end

# After: Added write for caching, but granted full write access
class ReportService
  def initialize
    @db = Database.connect(user: 'write_user')  # Too broad
  end

  def cache_report(report)
    @db.insert(:cached_reports, report)
  end
end

# Correct: Separate connections for different needs
class ReportService
  def read_connection
    @read_conn ||= Database.connect(user: 'readonly')
  end

  def cache_connection
    @cache_conn ||= Database.connect(user: 'cache_writer')
  end

  def cache_report(report)
    cache_connection.insert(:cached_reports, report)
  end
end

Development Convenience Shortcuts: Running applications with administrative privileges during development creates security habits that persist into production. Developers accustomed to unrestricted access may not notice missing permission checks until production deployment.

Overly Broad Database Grants: Database users often receive GRANT ALL or administrative privileges for simplicity. Applications should use database users with specific grants limited to required tables and operations.

# Problematic: Single database user with all privileges
DATABASE_USER = 'app_admin'  # Has GRANT ALL

# Better: Multiple users with specific grants
class DatabaseConnections
  def read_only
    PG.connect(user: 'app_reader')  # SELECT only on application tables
  end

  def transactional
    PG.connect(user: 'app_writer')  # INSERT, UPDATE, DELETE on data tables
  end

  def migrations
    PG.connect(user: 'app_migrations')  # DDL operations only
  end
end

Token Scope Neglect: API tokens and credentials often have broader scopes than necessary. Cloud service credentials might grant full account access when specific resource permissions suffice.

Error Message Information Leakage: Detailed error messages exposing internal state or permission structures help attackers map system boundaries. Error responses should not reveal whether resources exist or which specific permission failed.

# Leaks information about system structure
def fetch_resource(id)
  resource = Resource.find(id)
  
  unless can_access?(resource)
    raise Forbidden, "User #{current_user.id} lacks 'read' permission on resource #{id}"
  end
  
  resource
rescue ActiveRecord::RecordNotFound
  raise NotFound, "Resource #{id} does not exist"
end

# Better: Uniform responses
def fetch_resource(id)
  resource = Resource.find(id)
  
  unless can_access?(resource)
    raise NotFound  # Same response as missing resource
  end
  
  resource
rescue ActiveRecord::RecordNotFound
  raise NotFound
end

File Permission Defaults: Creating files without explicitly setting permissions relies on system umask, which may be permissive. Applications should set restrictive permissions on sensitive files.

Shared Credentials: Multiple services sharing credentials violates least privilege by preventing per-service access restriction. Compromising one service exposes credentials valid for all services.

Insufficient Privilege Separation in Testing: Test environments often run with elevated privileges to simplify test setup, but this hides authorization bugs that surface in production.

# Test that passes incorrectly
RSpec.describe UserController do
  before { allow_any_instance_of(UserController).to receive(:current_user).and_return(admin_user) }

  it 'updates user' do
    patch :update, params: { id: user.id, name: 'New Name' }
    expect(response).to be_successful
  end
end

# Test that verifies authorization
RSpec.describe UserController do
  context 'as regular user' do
    before { sign_in(regular_user) }

    it 'cannot update other users' do
      patch :update, params: { id: other_user.id, name: 'New Name' }
      expect(response).to be_forbidden
    end
  end

  context 'as admin' do
    before { sign_in(admin_user) }

    it 'can update any user' do
      patch :update, params: { id: user.id, name: 'New Name' }
      expect(response).to be_successful
    end
  end
end

Implicit Trust in Downstream Services: Applications trusting data or requests from downstream services without validation assume those services correctly enforce privileges. Compromise of a downstream service bypasses application security.

Practical Examples

Real-world scenarios demonstrate how to apply least privilege principles across different system components and architectures.

Multi-Tenant Application with Data Isolation: A SaaS application must ensure each tenant accesses only their data. Least privilege restricts database connections and queries to current tenant scope.

class MultiTenantDatabase
  def initialize(tenant_id)
    @tenant_id = tenant_id
    @connection = establish_connection(tenant_id)
  end

  def query(sql, params = [])
    # All queries automatically scoped to tenant
    scoped_sql = add_tenant_scope(sql)
    @connection.exec_params(scoped_sql, [@tenant_id] + params)
  end

  private

  def establish_connection(tenant_id)
    # Database user has RLS policy enforcing tenant_id
    PG.connect(
      dbname: 'multi_tenant',
      user: "tenant_#{tenant_id}",
      password: fetch_tenant_password(tenant_id)
    )
  end

  def add_tenant_scope(sql)
    # Parse and inject WHERE tenant_id = $1 clause
    # Simplified example - production would use query parser
    sql.gsub(/FROM (\w+)/, 'FROM \1 WHERE tenant_id = $1')
  end
end

class TenantController
  before_action :set_tenant

  def index
    @records = @tenant_db.query('SELECT * FROM records')
  end

  private

  def set_tenant
    @tenant_db = MultiTenantDatabase.new(current_tenant.id)
  end
end

Microservice Communication with Service Accounts: Microservices communicate using service-specific credentials rather than shared secrets. Each service has credentials granting access only to required endpoints.

class ServiceAuthenticator
  def initialize(service_name)
    @service_name = service_name
    @credentials = load_service_credentials(service_name)
  end

  def call_service(target_service, endpoint, data)
    token = generate_service_token(target_service)
    
    HTTP.auth("Bearer #{token}")
        .post("https://#{target_service}/#{endpoint}", json: data)
  end

  private

  def generate_service_token(target_service)
    # JWT with specific claims for service-to-service auth
    payload = {
      iss: @service_name,
      aud: target_service,
      exp: Time.now.to_i + 300,  # 5 minute expiry
      scopes: allowed_scopes(@service_name, target_service)
    }
    
    JWT.encode(payload, @credentials[:private_key], 'RS256')
  end

  def allowed_scopes(source, target)
    # Define allowed operations between services
    SERVICE_PERMISSIONS.dig(source, target) || []
  end
end

SERVICE_PERMISSIONS = {
  'user_service' => {
    'notification_service' => ['send_notification'],
    'billing_service' => ['check_subscription']
  },
  'order_service' => {
    'inventory_service' => ['reserve_items', 'release_items'],
    'billing_service' => ['charge_customer']
  }
}

Background Job Processing with Restricted Access: Background workers should not have access to production credentials or sensitive operations beyond their specific tasks.

class BackgroundWorker
  def perform(job_data)
    # Worker uses dedicated credentials with limited scope
    @connection = PG.connect(
      user: 'background_worker',
      password: ENV['WORKER_PASSWORD']
    )

    process_job(job_data)
  ensure
    @connection&.close
  end

  private

  def process_job(job_data)
    # Can only access worker-specific tables
    @connection.exec_params(
      'INSERT INTO job_results (data) VALUES ($1)',
      [job_data]
    )
  end
end

# Database grants for background_worker:
# GRANT INSERT, UPDATE ON job_results TO background_worker;
# GRANT SELECT ON job_queue TO background_worker;
# No access to user data, financial data, or administrative tables

API Gateway with Route-Specific Permissions: An API gateway enforces different permission requirements based on route and HTTP method.

class APIGateway
  ROUTE_PERMISSIONS = {
    'GET /users' => ['users:read'],
    'POST /users' => ['users:write'],
    'DELETE /users/:id' => ['users:delete'],
    'GET /admin/reports' => ['admin:read', 'reports:access'],
    'POST /admin/settings' => ['admin:write']
  }

  def call(env)
    request = Rack::Request.new(env)
    route_pattern = extract_route_pattern(request)
    
    required_permissions = ROUTE_PERMISSIONS[route_pattern]
    
    unless has_permissions?(request, required_permissions)
      return [403, {}, ['Forbidden']]
    end

    @app.call(env)
  end

  private

  def has_permissions?(request, required_permissions)
    token = extract_token(request)
    user_permissions = decode_permissions(token)
    
    required_permissions.all? { |perm| user_permissions.include?(perm) }
  end

  def extract_route_pattern(request)
    # Match request path to route pattern
    path = request.path_info
    method = request.request_method
    
    "#{method} #{normalize_path(path)}"
  end
end

File Upload with Quarantine and Scanning: User-uploaded files are processed with minimal privileges and isolated from production systems until validated.

class FileUploadProcessor
  QUARANTINE_DIR = '/var/quarantine'
  SAFE_DIR = '/var/uploads'

  def process_upload(uploaded_file)
    # Step 1: Save to quarantine with restricted permissions
    quarantine_path = save_to_quarantine(uploaded_file)
    
    # Step 2: Scan in isolated environment
    scan_result = scan_file(quarantine_path)
    
    unless scan_result.safe?
      File.delete(quarantine_path)
      raise SecurityError, 'File failed security scan'
    end
    
    # Step 3: Move to safe directory
    final_path = move_to_safe_storage(quarantine_path)
    
    # Step 4: Process with limited privileges
    process_with_restrictions(final_path)
  end

  private

  def save_to_quarantine(file)
    temp_path = File.join(QUARANTINE_DIR, SecureRandom.uuid)
    
    File.open(temp_path, 'wb', 0600) do |f|
      f.write(file.read)
    end
    
    # Quarantine directory owned by quarantine user, not app user
    temp_path
  end

  def scan_file(path)
    # Run scanner as separate user with read-only access
    scanner = Process.spawn(
      "sudo -u scanner /usr/bin/clamscan #{path}",
      out: '/tmp/scan.log'
    )
    
    Process.wait(scanner)
    ScanResult.new($?.exitstatus)
  end

  def move_to_safe_storage(source)
    destination = File.join(SAFE_DIR, File.basename(source))
    FileUtils.mv(source, destination)
    File.chmod(0644, destination)  # Read-only for application
    destination
  end
end

Reference

Permission Types and Scopes

Permission Type Description Scope Level
Read View data without modification Resource-specific
Write Create or modify data Resource-specific
Delete Remove data permanently Resource-specific
Execute Run operations or functions Function-specific
Admin Manage permissions and configuration System-wide
Owner Full control over specific resources Resource-specific

Access Control Models

Model Description Use Case
DAC Discretionary Access Control - owners control access File systems, documents
MAC Mandatory Access Control - system enforces policy Military, classified systems
RBAC Role-Based Access Control - access via roles Enterprise applications
ABAC Attribute-Based Access Control - context-aware Dynamic cloud environments

Ruby Security Methods

Method Purpose Example Context
File.open(path, mode) Restrict file operations by mode r for read-only, w for write-only
Process.uid Get current user ID Verify non-root execution
Process::Sys.setuid Drop privileges Startup privilege reduction
File.chmod(mode, path) Set file permissions Restrict access to sensitive files
FileUtils.rm(path, secure: true) Secure deletion Prevent path traversal attacks

Common Database Permission Grants

Grant Type Operations Allowed Typical Use
SELECT Read operations only Reporting, analytics
INSERT Create new records Data collection services
UPDATE Modify existing records User preference updates
DELETE Remove records Data retention cleanup
EXECUTE Run stored procedures Controlled business logic
CONNECT Establish connection Basic database access

Privilege Escalation Patterns

Pattern Mechanism Duration
Sudo-style Re-authentication required Single operation
Time-limited token Temporary elevated token Minutes to hours
Approval workflow Manager authorization Operation-specific
Break-glass Emergency access with audit Immediate, logged

Security Principle Hierarchy

Principle Relationship to Least Privilege Priority
Defense in Depth Least privilege creates multiple barriers High
Fail-Safe Defaults Denies access unless explicitly granted High
Separation of Duties Different privileges for different functions Medium
Complete Mediation Every access requires privilege check High
Least Common Mechanism Minimizes shared high-privilege components Medium

Ruby Permission Check Patterns

# Before action pattern
before_action :require_permission

def require_permission
  unless current_user.has_permission?(required_permission)
    raise Forbidden
  end
end

# Policy object pattern
def authorize_action
  policy = ResourcePolicy.new(current_user, resource)
  raise Forbidden unless policy.allowed?(action)
end

# Capability pattern
def with_capability(capability)
  raise SecurityError unless capability.valid?
  yield capability
end

Environment Variable Access Patterns

Pattern Security Level Use Case
Direct ENV access Low - unrestricted Non-sensitive configuration
Restricted wrapper Medium - allowlist Service-specific config
Encrypted secrets High - encrypted at rest Credentials, tokens
Vault integration Highest - dynamic secrets Production credentials

File Permission Modes

Octal Symbolic Description Use Case
0600 rw------- Owner read/write only Private keys, secrets
0644 rw-r--r-- Owner write, others read Configuration files
0400 r-------- Owner read only Read-only credentials
0700 rwx------ Owner full access Private executables
0755 rwxr-xr-x Owner write, others execute Public executables