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
- Authentication and Authorization
- Dashboard Security
- Data Protection
- Network Security
- Secrets Management
- Audit Logging
- Compliance
- Security Checklist
- Incident Response
Security Principles¶
Defense in Depth¶
Implement multiple layers of security controls:
- Network Layer: Firewalls, VPNs, private networks
- Application Layer: Authentication, authorization, input validation
- Data Layer: Encryption at rest and in transit, access controls
- 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);
}
}
JWT Authentication (Recommended)¶
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¶
- Deployment Guide - Securely deploy Flaggy
- Best Practices - Follow recommended patterns
- Performance Optimization - Maintain security with performance
- Monitoring - Set up security monitoring