Skip to content

Security Best Practices

Overview

This comprehensive guide covers security best practices for deploying and managing Flaggy in production environments. It includes authentication, authorization, data protection, network security, audit logging, and compliance considerations.

Table of Contents

Security Principles

Defense in Depth

Implement multiple layers of security controls:

  1. Network Layer: Firewalls, VPNs, private networks
  2. Application Layer: Authentication, authorization, input validation
  3. Data Layer: Encryption at rest and in transit, access controls
  4. Monitoring Layer: Audit logs, intrusion detection, anomaly detection

Least Privilege Principle

Grant minimum necessary permissions:

// Bad: Over-permissioned database user
GRANT ALL PRIVILEGES ON *.* TO 'app_user'@'%';

// Good: Minimal required permissions
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp_prod.feature_flags TO 'app_user'@'%';
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp_prod.users TO 'app_user'@'%';
GRANT SELECT, INSERT ON myapp_prod.flaggy_migrations TO 'app_user'@'%';

Fail Securely

When security controls fail, default to a secure state:

public async Task<bool> IsEnabledAsync(string key)
{
    try
    {
        return await _flagService.IsEnabledAsync(key);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to evaluate flag {Key}", key);
        // Fail closed - deny access by default
        return false;
    }
}

Zero Trust Architecture

Never trust, always verify:

// Verify every request, even from internal networks
app.UseFlaggyUI(options =>
{
    options.RequireAuthorization = true;
    options.AuthorizationFilter = context =>
    {
        // Check authentication
        if (!context.User.Identity?.IsAuthenticated ?? true)
            return false;

        // Check authorization
        if (!context.User.IsInRole("Admin"))
            return false;

        // Check IP whitelist
        var clientIp = context.Connection.RemoteIpAddress?.ToString();
        if (!IsIpWhitelisted(clientIp))
            return false;

        // Check rate limiting
        if (IsRateLimitExceeded(context.User.Identity.Name))
            return false;

        return true;
    };
});

Authentication and Authorization

Dashboard Authentication

Basic Authentication (Development Only)

// WARNING: Only use in development environments
public class BasicAuthMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _username;
    private readonly string _password;

    public BasicAuthMiddleware(RequestDelegate next, IConfiguration config)
    {
        _next = next;
        _username = config["BasicAuth:Username"];
        _password = config["BasicAuth:Password"];
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (!context.Request.Path.StartsWithSegments("/flaggy"))
        {
            await _next(context);
            return;
        }

        var authHeader = context.Request.Headers["Authorization"].ToString();
        if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic "))
        {
            context.Response.StatusCode = 401;
            context.Response.Headers.Add("WWW-Authenticate", "Basic realm=\"Flaggy\"");
            return;
        }

        var encodedCredentials = authHeader.Substring("Basic ".Length).Trim();
        var credentials = Encoding.UTF8.GetString(Convert.FromBase64String(encodedCredentials));
        var parts = credentials.Split(':', 2);

        if (parts.Length != 2 || parts[0] != _username || parts[1] != _password)
        {
            context.Response.StatusCode = 401;
            return;
        }

        await _next(context);
    }
}
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

// Configure JWT authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])
            ),
            ClockSkew = TimeSpan.Zero
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin"));

    options.AddPolicy("FlagManagement", policy =>
        policy.RequireRole("Admin", "FlagManager"));
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

// Secure dashboard with JWT
app.UseFlaggyUI(options =>
{
    options.RoutePrefix = "/admin/flags";
    options.RequireAuthorization = true;
    options.AuthorizationFilter = context =>
    {
        return context.User.IsInRole("Admin") || context.User.IsInRole("FlagManager");
    };
});

Azure Active Directory Integration

using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

// Configure Azure AD authentication
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("FlaggyAccess", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireRole("FlaggyAdmin");
    });
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

// Secure with Azure AD
app.UseFlaggyUI(options =>
{
    options.RequireAuthorization = true;
    options.AuthorizationFilter = context =>
    {
        return context.User.IsInRole("FlaggyAdmin");
    };
});

User Management Security

Password Security

Flaggy uses BCrypt for password hashing. Always follow these practices:

// Flaggy automatically uses BCrypt for password hashing
await _userProvider.CreateUserAsync(new User
{
    Username = "admin",
    Password = "SecurePassword123!" // Will be hashed with BCrypt
});

// Never store or log plain-text passwords
// BAD:
_logger.LogInformation("User {Username} logged in with password {Password}", username, password);

// GOOD:
_logger.LogInformation("User {Username} logged in successfully", username);

Password Policy

Enforce strong password policies:

public class PasswordValidator
{
    private const int MinLength = 12;
    private const int RequiredUppercase = 1;
    private const int RequiredLowercase = 1;
    private const int RequiredDigits = 1;
    private const int RequiredSpecialChars = 1;

    public static (bool IsValid, string ErrorMessage) Validate(string password)
    {
        if (string.IsNullOrEmpty(password))
            return (false, "Password is required");

        if (password.Length < MinLength)
            return (false, $"Password must be at least {MinLength} characters");

        if (password.Count(char.IsUpper) < RequiredUppercase)
            return (false, $"Password must contain at least {RequiredUppercase} uppercase letter");

        if (password.Count(char.IsLower) < RequiredLowercase)
            return (false, $"Password must contain at least {RequiredLowercase} lowercase letter");

        if (password.Count(char.IsDigit) < RequiredDigits)
            return (false, $"Password must contain at least {RequiredDigits} digit");

        var specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
        if (password.Count(c => specialChars.Contains(c)) < RequiredSpecialChars)
            return (false, $"Password must contain at least {RequiredSpecialChars} special character");

        // Check against common passwords
        if (IsCommonPassword(password))
            return (false, "Password is too common, please choose a stronger password");

        return (true, string.Empty);
    }

    private static bool IsCommonPassword(string password)
    {
        var commonPasswords = new HashSet<string>
        {
            "Password123!", "Admin123!", "Welcome123!", "Change123!"
            // Load from file or database
        };

        return commonPasswords.Contains(password);
    }
}

// Usage
public async Task<User> CreateUserWithValidation(string username, string password)
{
    var (isValid, errorMessage) = PasswordValidator.Validate(password);
    if (!isValid)
    {
        throw new ValidationException(errorMessage);
    }

    return await _userProvider.CreateUserAsync(new User
    {
        Username = username,
        Password = password
    });
}

Account Lockout

Implement account lockout after failed login attempts:

public class LoginAttemptTracker
{
    private readonly IMemoryCache _cache;
    private const int MaxAttempts = 5;
    private static readonly TimeSpan LockoutDuration = TimeSpan.FromMinutes(15);

    public bool IsLocked(string username)
    {
        var key = $"lockout:{username}";
        return _cache.TryGetValue(key, out _);
    }

    public void RecordFailedAttempt(string username)
    {
        var key = $"attempts:{username}";
        var attempts = _cache.GetOrCreate(key, entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
            return 0;
        });

        attempts++;
        _cache.Set(key, attempts);

        if (attempts >= MaxAttempts)
        {
            var lockoutKey = $"lockout:{username}";
            _cache.Set(lockoutKey, true, LockoutDuration);
            _logger.LogWarning("Account {Username} locked due to too many failed login attempts", username);
        }
    }

    public void ResetAttempts(string username)
    {
        var key = $"attempts:{username}";
        _cache.Remove(key);
    }
}

Dashboard Security

IP Whitelisting

Restrict dashboard access to specific IP addresses:

public class IpWhitelistMiddleware
{
    private readonly RequestDelegate _next;
    private readonly HashSet<string> _allowedIps;
    private readonly ILogger<IpWhitelistMiddleware> _logger;

    public IpWhitelistMiddleware(
        RequestDelegate next,
        IConfiguration config,
        ILogger<IpWhitelistMiddleware> logger)
    {
        _next = next;
        _logger = logger;

        var allowedIps = config.GetSection("Security:AllowedIPs").Get<string[]>() ?? Array.Empty<string>();
        _allowedIps = new HashSet<string>(allowedIps, StringComparer.OrdinalIgnoreCase);
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (!context.Request.Path.StartsWithSegments("/flaggy"))
        {
            await _next(context);
            return;
        }

        var remoteIp = context.Connection.RemoteIpAddress?.ToString();

        if (string.IsNullOrEmpty(remoteIp) || !_allowedIps.Contains(remoteIp))
        {
            _logger.LogWarning("Blocked dashboard access from IP: {IpAddress}", remoteIp);
            context.Response.StatusCode = 403;
            await context.Response.WriteAsync("Access denied");
            return;
        }

        await _next(context);
    }
}

// Register middleware
app.UseMiddleware<IpWhitelistMiddleware>();

CIDR Range Support

Support IP ranges using CIDR notation:

using System.Net;

public static class IpAddressExtensions
{
    public static bool IsInRange(this IPAddress address, string cidrRange)
    {
        var parts = cidrRange.Split('/');
        if (parts.Length != 2)
            return false;

        var baseAddress = IPAddress.Parse(parts[0]);
        var prefixLength = int.Parse(parts[1]);

        var addressBytes = address.GetAddressBytes();
        var baseBytes = baseAddress.GetAddressBytes();

        if (addressBytes.Length != baseBytes.Length)
            return false;

        var mask = CreateMask(prefixLength, addressBytes.Length);

        for (int i = 0; i < addressBytes.Length; i++)
        {
            if ((addressBytes[i] & mask[i]) != (baseBytes[i] & mask[i]))
                return false;
        }

        return true;
    }

    private static byte[] CreateMask(int prefixLength, int byteCount)
    {
        var mask = new byte[byteCount];
        var bitsRemaining = prefixLength;

        for (int i = 0; i < byteCount; i++)
        {
            if (bitsRemaining >= 8)
            {
                mask[i] = 0xFF;
                bitsRemaining -= 8;
            }
            else if (bitsRemaining > 0)
            {
                mask[i] = (byte)(0xFF << (8 - bitsRemaining));
                bitsRemaining = 0;
            }
            else
            {
                mask[i] = 0x00;
            }
        }

        return mask;
    }
}

// Usage
app.UseFlaggyUI(options =>
{
    options.AuthorizationFilter = context =>
    {
        var clientIp = context.Connection.RemoteIpAddress;
        var allowedRanges = new[] { "10.0.0.0/8", "172.16.0.0/12", "192.168.1.0/24" };

        return allowedRanges.Any(range => clientIp.IsInRange(range));
    };
});

Rate Limiting

Prevent brute force attacks with rate limiting:

using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

// Configure rate limiting
builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
    {
        var username = context.User.Identity?.Name ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous";

        return RateLimitPartition.GetFixedWindowLimiter(username, partition =>
            new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1),
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = 0
            });
    });

    options.OnRejected = async (context, cancellationToken) =>
    {
        context.HttpContext.Response.StatusCode = 429;
        await context.HttpContext.Response.WriteAsync(
            "Too many requests. Please try again later.",
            cancellationToken);
    };
});

var app = builder.Build();
app.UseRateLimiter();

CSRF Protection

Protect against Cross-Site Request Forgery:

var builder = WebApplication.CreateBuilder(args);

// Configure anti-forgery
builder.Services.AddAntiforgery(options =>
{
    options.HeaderName = "X-CSRF-TOKEN";
    options.Cookie.Name = "X-CSRF-TOKEN";
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Strict;
});

var app = builder.Build();

// Add CSRF token to responses
app.Use(async (context, next) =>
{
    if (context.Request.Path.StartsWithSegments("/flaggy"))
    {
        var antiforgery = context.RequestServices.GetRequiredService<IAntiforgery>();
        var tokens = antiforgery.GetAndStoreTokens(context);
        context.Response.Headers.Add("X-CSRF-TOKEN", tokens.RequestToken);
    }

    await next();
});

Data Protection

Encryption at Rest

Database Encryption

-- MySQL: Enable encryption at rest
-- Create encrypted tablespace
CREATE TABLESPACE encrypted_ts
  ADD DATAFILE 'encrypted_ts.ibd'
  ENCRYPTION = 'Y';

-- Create table in encrypted tablespace
CREATE TABLE feature_flags (
    `Key` VARCHAR(255) PRIMARY KEY,
    IsEnabled BOOLEAN NOT NULL DEFAULT FALSE,
    `Value` TEXT NULL,
    Description TEXT NULL,
    CreatedAt DATETIME NULL,
    UpdatedAt DATETIME NULL
) TABLESPACE encrypted_ts;

Application-Level Encryption

using System.Security.Cryptography;

public class EncryptedFeatureFlagService : IFeatureFlagService
{
    private readonly IFeatureFlagService _inner;
    private readonly byte[] _key;
    private readonly byte[] _iv;

    public EncryptedFeatureFlagService(IFeatureFlagService inner, IConfiguration config)
    {
        _inner = inner;
        _key = Convert.FromBase64String(config["Encryption:Key"]);
        _iv = Convert.FromBase64String(config["Encryption:IV"]);
    }

    public async Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken ct = default)
    {
        // Encrypt sensitive data before storing
        if (!string.IsNullOrEmpty(flag.Value))
        {
            flag.Value = Encrypt(flag.Value);
        }

        return await _inner.CreateFlagAsync(flag, ct);
    }

    public async Task<FeatureFlag?> GetFlagAsync(string key, CancellationToken ct = default)
    {
        var flag = await _inner.GetFlagAsync(key, ct);

        if (flag != null && !string.IsNullOrEmpty(flag.Value))
        {
            flag.Value = Decrypt(flag.Value);
        }

        return flag;
    }

    private string Encrypt(string plainText)
    {
        using var aes = Aes.Create();
        aes.Key = _key;
        aes.IV = _iv;

        using var encryptor = aes.CreateEncryptor();
        var plainBytes = Encoding.UTF8.GetBytes(plainText);
        var encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);

        return Convert.ToBase64String(encryptedBytes);
    }

    private string Decrypt(string cipherText)
    {
        using var aes = Aes.Create();
        aes.Key = _key;
        aes.IV = _iv;

        using var decryptor = aes.CreateDecryptor();
        var cipherBytes = Convert.FromBase64String(cipherText);
        var decryptedBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);

        return Encoding.UTF8.GetString(decryptedBytes);
    }
}

Encryption in Transit

SSL/TLS Configuration

var builder = WebApplication.CreateBuilder(args);

// Force HTTPS
builder.Services.AddHttpsRedirection(options =>
{
    options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
    options.HttpsPort = 443;
});

// Configure HSTS
builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(365);
});

var app = builder.Build();

// Use HSTS in production
if (app.Environment.IsProduction())
{
    app.UseHsts();
}

app.UseHttpsRedirection();

Database SSL Connections

// MySQL with SSL
builder.Services.AddFlaggy(options =>
{
    options.UseMySQL(
        connectionString: "Server=prod-db.company.com;Database=myapp;User=app_user;Password=pass;SslMode=Required;SslCert=/path/to/client-cert.pem;SslKey=/path/to/client-key.pem;SslCa=/path/to/ca-cert.pem;"
    );
});

// PostgreSQL with SSL
builder.Services.AddFlaggy(options =>
{
    options.UsePostgreSQL(
        connectionString: "Host=prod-db.company.com;Database=myapp;Username=app_user;Password=pass;SSL Mode=Require;Trust Server Certificate=false;SSL Certificate=/path/to/client-cert.pem;SSL Key=/path/to/client-key.pem;Root Certificate=/path/to/ca-cert.pem;"
    );
});

// Redis with SSL
builder.Services.AddFlaggy(options =>
{
    options.UseMySQL(connectionString: "..."); // or UsePostgreSQL, UseMsSql
    options.UseRedisCache(
        "prod-redis.company.com:6380,ssl=true,password=pass,sslCertificateFile=/path/to/client.pfx",
        TimeSpan.FromMinutes(10)
    );
});

Data Masking

Mask sensitive data in logs and responses:

public class DataMaskingService
{
    public static string MaskValue(string value, int visibleChars = 4)
    {
        if (string.IsNullOrEmpty(value) || value.Length <= visibleChars)
            return new string('*', value?.Length ?? 0);

        return value.Substring(0, visibleChars) + new string('*', value.Length - visibleChars);
    }

    public static FeatureFlag MaskFlag(FeatureFlag flag)
    {
        return new FeatureFlag
        {
            Key = flag.Key,
            IsEnabled = flag.IsEnabled,
            Value = MaskValue(flag.Value),
            Description = flag.Description,
            CreatedAt = flag.CreatedAt,
            UpdatedAt = flag.UpdatedAt
        };
    }
}

// Usage in logging
_logger.LogInformation(
    "Flag updated: {Key}, Value: {Value}",
    flag.Key,
    DataMaskingService.MaskValue(flag.Value)
);

Network Security

Firewall Rules

# Allow only necessary ports
# MySQL
sudo ufw allow from 10.0.0.0/8 to any port 3306

# Redis
sudo ufw allow from 10.0.0.0/8 to any port 6379

# HTTPS
sudo ufw allow 443/tcp

# SSH (limited to bastion host)
sudo ufw allow from 10.0.0.10 to any port 22

# Enable firewall
sudo ufw enable

VPC and Security Groups (AWS)

{
  "SecurityGroup": {
    "GroupName": "flaggy-app-sg",
    "Description": "Security group for Flaggy application",
    "VpcId": "vpc-12345678",
    "IpPermissions": [
      {
        "IpProtocol": "tcp",
        "FromPort": 443,
        "ToPort": 443,
        "IpRanges": [{ "CidrIp": "0.0.0.0/0" }]
      },
      {
        "IpProtocol": "tcp",
        "FromPort": 3306,
        "ToPort": 3306,
        "UserIdGroupPairs": [{ "GroupId": "sg-app-servers" }]
      },
      {
        "IpProtocol": "tcp",
        "FromPort": 6379,
        "ToPort": 6379,
        "UserIdGroupPairs": [{ "GroupId": "sg-app-servers" }]
      }
    ]
  }
}

Private Network Deployment

# Kubernetes NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: flaggy-network-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: myapp
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          name: production
    - podSelector:
        matchLabels:
          app: load-balancer
    ports:
    - protocol: TCP
      port: 8080
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: mysql
    ports:
    - protocol: TCP
      port: 3306
  - to:
    - podSelector:
        matchLabels:
          app: redis
    ports:
    - protocol: TCP
      port: 6379

Secrets Management

Azure Key Vault

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

public class KeyVaultSecretProvider
{
    private readonly SecretClient _client;

    public KeyVaultSecretProvider(string keyVaultUrl)
    {
        _client = new SecretClient(new Uri(keyVaultUrl), new DefaultAzureCredential());
    }

    public async Task<string> GetSecretAsync(string secretName)
    {
        var secret = await _client.GetSecretAsync(secretName);
        return secret.Value.Value;
    }
}

// Usage
var secretProvider = new KeyVaultSecretProvider("https://mykeyvault.vault.azure.net");
var dbPassword = await secretProvider.GetSecretAsync("DbPassword");
var connectionString = $"Server=prod-db;Database=myapp;User=app_user;Password={dbPassword};";

AWS Secrets Manager

using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;

public class AwsSecretProvider
{
    private readonly IAmazonSecretsManager _client;

    public AwsSecretProvider()
    {
        _client = new AmazonSecretsManagerClient(Amazon.RegionEndpoint.USEast1);
    }

    public async Task<string> GetSecretAsync(string secretId)
    {
        var request = new GetSecretValueRequest { SecretId = secretId };
        var response = await _client.GetSecretValueAsync(request);
        return response.SecretString;
    }
}

// Usage
var secretProvider = new AwsSecretProvider();
var secretJson = await secretProvider.GetSecretAsync("prod/myapp/database");
var secrets = JsonSerializer.Deserialize<DatabaseSecrets>(secretJson);

HashiCorp Vault

using VaultSharp;
using VaultSharp.V1.AuthMethods.Token;

public class VaultSecretProvider
{
    private readonly IVaultClient _client;

    public VaultSecretProvider(string vaultAddress, string token)
    {
        var authMethod = new TokenAuthMethodInfo(token);
        var vaultClientSettings = new VaultClientSettings(vaultAddress, authMethod);
        _client = new VaultClient(vaultClientSettings);
    }

    public async Task<string> GetSecretAsync(string path, string key)
    {
        var secret = await _client.V1.Secrets.KeyValue.V2.ReadSecretAsync(path);
        return secret.Data.Data[key].ToString();
    }
}

Environment-Specific Secrets

# Development
export DB_PASSWORD="dev-password"
export REDIS_PASSWORD="dev-redis-password"

# Staging
export DB_PASSWORD="staging-password-$(openssl rand -base64 32)"
export REDIS_PASSWORD="staging-redis-$(openssl rand -base64 32)"

# Production - Use secrets manager
# Do not set production secrets as environment variables

Audit Logging

Comprehensive Audit Log

public class AuditLog
{
    public Guid Id { get; set; }
    public DateTime Timestamp { get; set; }
    public string Action { get; set; }
    public string ResourceType { get; set; }
    public string ResourceId { get; set; }
    public string UserId { get; set; }
    public string Username { get; set; }
    public string IpAddress { get; set; }
    public string UserAgent { get; set; }
    public string BeforeValue { get; set; }
    public string AfterValue { get; set; }
    public bool Success { get; set; }
    public string ErrorMessage { get; set; }
}

public class AuditedFeatureFlagService : IFeatureFlagService
{
    private readonly IFeatureFlagService _inner;
    private readonly IAuditLogger _auditLogger;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public async Task<bool> UpdateFlagAsync(FeatureFlag flag, CancellationToken ct = default)
    {
        var context = _httpContextAccessor.HttpContext;
        var before = await _inner.GetFlagAsync(flag.Key, ct);

        try
        {
            var result = await _inner.UpdateFlagAsync(flag, ct);

            await _auditLogger.LogAsync(new AuditLog
            {
                Id = Guid.NewGuid(),
                Timestamp = DateTime.UtcNow,
                Action = "UpdateFlag",
                ResourceType = "FeatureFlag",
                ResourceId = flag.Key,
                UserId = context?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value,
                Username = context?.User.Identity?.Name,
                IpAddress = context?.Connection.RemoteIpAddress?.ToString(),
                UserAgent = context?.Request.Headers["User-Agent"].ToString(),
                BeforeValue = JsonSerializer.Serialize(before),
                AfterValue = JsonSerializer.Serialize(flag),
                Success = result,
                ErrorMessage = null
            });

            return result;
        }
        catch (Exception ex)
        {
            await _auditLogger.LogAsync(new AuditLog
            {
                Id = Guid.NewGuid(),
                Timestamp = DateTime.UtcNow,
                Action = "UpdateFlag",
                ResourceType = "FeatureFlag",
                ResourceId = flag.Key,
                UserId = context?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value,
                Username = context?.User.Identity?.Name,
                IpAddress = context?.Connection.RemoteIpAddress?.ToString(),
                UserAgent = context?.Request.Headers["User-Agent"].ToString(),
                BeforeValue = JsonSerializer.Serialize(before),
                AfterValue = JsonSerializer.Serialize(flag),
                Success = false,
                ErrorMessage = ex.Message
            });

            throw;
        }
    }
}

Structured Logging

using Serilog;
using Serilog.Events;

var builder = WebApplication.CreateBuilder(args);

// Configure Serilog
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .MinimumLevel.Override("Flaggy", LogEventLevel.Information)
    .Enrich.FromLogContext()
    .Enrich.WithProperty("Application", "MyApp")
    .Enrich.WithProperty("Environment", builder.Environment.EnvironmentName)
    .WriteTo.Console()
    .WriteTo.File(
        path: "logs/flaggy-.log",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 30,
        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
    .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("https://elasticsearch:9200"))
    {
        AutoRegisterTemplate = true,
        IndexFormat = "flaggy-audit-{0:yyyy.MM.dd}"
    })
    .CreateLogger();

builder.Host.UseSerilog();

Log Retention

-- Create audit log table
CREATE TABLE audit_logs (
    id CHAR(36) PRIMARY KEY,
    timestamp DATETIME NOT NULL,
    action VARCHAR(100) NOT NULL,
    resource_type VARCHAR(100) NOT NULL,
    resource_id VARCHAR(255) NOT NULL,
    user_id VARCHAR(255),
    username VARCHAR(255),
    ip_address VARCHAR(50),
    user_agent VARCHAR(500),
    before_value TEXT,
    after_value TEXT,
    success BOOLEAN NOT NULL,
    error_message TEXT,
    INDEX idx_timestamp (timestamp),
    INDEX idx_user_id (user_id),
    INDEX idx_resource (resource_type, resource_id)
);

-- Create retention policy (remove logs older than 2 years)
CREATE EVENT cleanup_old_audit_logs
ON SCHEDULE EVERY 1 DAY
DO
  DELETE FROM audit_logs WHERE timestamp < DATE_SUB(NOW(), INTERVAL 2 YEAR);

Compliance

GDPR Compliance

Right to Access

public class GdprService
{
    private readonly IAuditLogger _auditLogger;

    public async Task<UserDataExport> ExportUserData(string userId)
    {
        return new UserDataExport
        {
            UserId = userId,
            AuditLogs = await _auditLogger.GetUserAuditLogsAsync(userId),
            CreatedFlags = await GetFlagsCreatedByUser(userId),
            ModifiedFlags = await GetFlagsModifiedByUser(userId)
        };
    }
}

Right to Erasure

public async Task AnonymizeUserData(string userId)
{
    // Anonymize user in audit logs
    await _db.ExecuteSqlAsync(
        "UPDATE audit_logs SET user_id = NULL, username = 'anonymized', ip_address = NULL WHERE user_id = @userId",
        new { userId }
    );

    // Delete user account
    await _userProvider.DeleteUserAsync(userId);
}

SOC 2 Compliance

Access Controls

  • Implement role-based access control (RBAC)
  • Use multi-factor authentication (MFA)
  • Regular access reviews
  • Least privilege principle

Audit Trails

  • Log all flag changes
  • Log all user authentications
  • Log all access attempts
  • Retain logs for required period

Encryption

  • Encrypt data at rest
  • Encrypt data in transit
  • Secure key management

HIPAA Compliance

// Ensure PHI is never stored in flags
public class HipaaComplianceValidator
{
    private static readonly Regex[] PhiPatterns = new[]
    {
        new Regex(@"\b\d{3}-\d{2}-\d{4}\b"), // SSN
        new Regex(@"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", RegexOptions.IgnoreCase), // Email
        new Regex(@"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b"), // Phone
    };

    public static bool ContainsPhi(string value)
    {
        if (string.IsNullOrEmpty(value))
            return false;

        return PhiPatterns.Any(pattern => pattern.IsMatch(value));
    }
}

// Validate before storing
public async Task<bool> CreateFlagAsync(FeatureFlag flag)
{
    if (HipaaComplianceValidator.ContainsPhi(flag.Value) ||
        HipaaComplianceValidator.ContainsPhi(flag.Description))
    {
        throw new SecurityException("PHI detected in flag data");
    }

    return await _inner.CreateFlagAsync(flag);
}

Security Checklist

Pre-Deployment Checklist

  • Authentication configured and tested
  • Authorization policies defined
  • Strong password policy enforced
  • Account lockout implemented
  • Dashboard access restricted
  • IP whitelist configured
  • Rate limiting enabled
  • CSRF protection enabled
  • SSL/TLS enabled for all connections
  • Database connections encrypted
  • Redis connections encrypted
  • Secrets stored in vault (not code/config)
  • Sensitive data encrypted at rest
  • Audit logging implemented
  • Log retention policy defined
  • Security headers configured
  • Firewall rules configured
  • Network segmentation implemented
  • Regular backups scheduled
  • Disaster recovery plan documented

Post-Deployment Checklist

  • Security scan completed
  • Penetration testing performed
  • Vulnerability assessment done
  • Compliance requirements verified
  • Incident response plan tested
  • Security monitoring configured
  • Alerts set up for suspicious activity
  • Regular security reviews scheduled
  • Team security training completed

Incident Response

Incident Response Plan

1. Detection and Analysis

// Monitor for suspicious activity
public class SecurityMonitor
{
    public void MonitorForAnomalies()
    {
        // Failed login attempts
        if (failedLogins > threshold)
            AlertSecurityTeam("Multiple failed login attempts detected");

        // Unusual access patterns
        if (accessTimeOutsideBusinessHours)
            AlertSecurityTeam("Access outside business hours");

        // Large number of flag changes
        if (flagChangesPerHour > normalThreshold * 3)
            AlertSecurityTeam("Unusual flag change activity");

        // Access from new IP addresses
        if (isNewIpAddress && isProductionEnvironment)
            AlertSecurityTeam("Access from new IP address");
    }
}

2. Containment

// Emergency lockdown
public async Task EmergencyLockdown()
{
    // Disable dashboard access
    await _configService.SetAsync("Dashboard:Enabled", false);

    // Revoke all active sessions
    await _sessionService.RevokeAllSessionsAsync();

    // Enable IP whitelist with only trusted IPs
    await _configService.SetAsync("Security:IpWhitelistEnabled", true);
    await _configService.SetAsync("Security:AllowedIps", new[] { "10.0.0.1" });

    // Alert team
    await _notificationService.NotifySecurityTeamAsync("Emergency lockdown activated");
}

3. Eradication

// Remove compromised accounts
public async Task RemediateSecurityBreach()
{
    // Reset all user passwords
    await _userService.ForcePasswordResetForAllUsersAsync();

    // Rotate all secrets
    await _secretService.RotateAllSecretsAsync();

    // Review and remove unauthorized flags
    var suspiciousFlags = await _flagService.GetFlagsCreatedAfter(incidentStartTime);
    foreach (var flag in suspiciousFlags)
    {
        await _flagService.DeleteFlagAsync(flag.Key);
    }
}

4. Recovery

// Restore from backup
public async Task RestoreFromBackup(DateTime backupDate)
{
    // Stop application
    await _applicationService.StopAsync();

    // Restore database
    await _databaseService.RestoreBackupAsync(backupDate);

    // Verify data integrity
    var isValid = await _databaseService.VerifyIntegrityAsync();
    if (!isValid)
        throw new Exception("Data integrity check failed");

    // Start application
    await _applicationService.StartAsync();

    // Verify functionality
    await _healthCheckService.RunAllChecksAsync();
}

5. Post-Incident Review

Document lessons learned and improve security posture.

Next Steps