07Безопасность (Security)
OWASP Top 10 для .NET
# OWASP Top 10 для .NET
Введение
OWASP (Open Web Application Security Project) определяет критические риски безопасности веб-приложений. Top 10 обновляется ~2-3 года и является стандартом для оценки уязвимостей.
A01:2021 — Broken Access Control
Суть
Приложение не проверяет права пользователя на действие или доступ к данным.
Типы атак
- IDOR — доступ к чужим ресурсам через изменение ID в URL
- MFLAC — несекьюрные API-эндпоинты без [Authorize]
- Privilege escalation — повышение прав
Уязвимый пример
\\\csharp // Нет проверки прав [HttpGet("/admin/data")] public IActionResult GetAdminData() => Ok("Sensitive admin data");
// IDOR — пользователь получает чужой заказ [HttpGet("/orders/{orderId}")] public IActionResult GetOrder(int orderId) { var order = _context.Orders.Find(orderId); // нет проверки ownership return Ok(order); } \\\
Защита
\\\csharp // RBAC [Authorize(Roles = "Admin")] [HttpGet("/admin/data")] public IActionResult GetAdminData() => Ok("Admin data");
// Защита от IDOR [HttpGet("/orders/{orderId}")] public IActionResult GetOrder(int orderId) { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); var order = _context.Orders.FirstOrDefault(o => o.Id == orderId && o.UserId == userId); if (order == null) return Forbid(); return Ok(order); }
// Глобальная политика — все API требуют авторизации builder.Services.AddControllers(config => { var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); config.Filters.Add(new AuthorizeFilter(policy)); }); \\\
A02:2021 — Cryptographic Failures
Суть
Сенситивные данные не шифруются at rest или in transit.
Что шифровать
- Пароли (Argon2id, bcrypt, PBKDF2)
- PII (персональные данные)
- Payment data, connection strings, API keys
Защита
\\\csharp // ASP.NET Core Identity — PBKDF2 с per-user salt по умолчанию services.AddIdentity
// Data Protection API для шифрования значений services.AddDataProtection() .SetApplicationName("SecureApp") .SetDefaultKeyLifetime(TimeSpan.FromDays(90));
public class DataProtectionService { private readonly IDataProtectionProvider _dp; public DataProtectionService(IDataProtectionProvider provider) => _dp = provider;
public string Protect(string data) { var protector = _dp.CreateProtector("my-purpose"); return protector.Protect(data); }
public string Unprotect(string protectedData) { var protector = _dp.CreateProtector("my-purpose"); return protector.Unprotect(protectedData); } } \\\
A03:2021 — Injection
Типы инъекций
| Тип | Цель | Пример | |
|---|---|---|---|
| SQL Injection | БД | ' OR 1=1 -- | |
| Command Injection | ОС | ; rm -rf / | |
| LDAP Injection | LDAP | )(uid=)( | |
| XXE | XML parser | ||
| NoSQL Injection | MongoDB | {"\: ""} |
SQL Injection — защита
\\\csharp // Уязвимо public IActionResult Search(string email) { var sql = $"SELECT * FROM Users WHERE Email = '{email}'"; var users = _context.Users.FromSqlRaw(sql).ToList(); return Ok(users); }
// Защита: EF Core параметризует автоматически public IActionResult Search(string email) { var users = _context.Users.Where(u => u.Email == email).ToList(); return Ok(users); } \\\
XXE — защита
\\\csharp var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }; var reader = XmlReader.Create(xmlStream, settings); \\\
A04:2021 — Insecure Design
Методологии
- STRIDE — Spoofing, Tampering, Repudiation, Information Disclosure, DoS, Elevation of Privilege
- DREAD — Damage, Reproducibility, Exploitability, Affected users, Discoverability
- Attack Trees — декомпозиция атак
Secure Design
\\\csharp // Защита: security by design [Authorize] [HttpPost("/transfer")] public IActionResult TransferFunds(decimal amount, int accountId) { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); if (!_bank.CanAccessAccount(userId, accountId)) return Forbid(); if (!_bank.HasSufficientBalance(userId, amount)) return BadRequest(); _bank.Transfer(amount, accountId); return Ok("Transferred"); } \\\
A05:2021 — Security Misconfiguration
Частые ошибки
- Default credentials, verbose errors, debug mode
- Открытые CORS, missing security headers
- Лишние HTTP methods (TRACE)
Защита
\\\csharp // Environment-specific if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); }
app.UseHttpsRedirection();
// Security headers app.Use(async (context, next) => { context.Response.Headers["X-Content-Type-Options"] = "nosniff"; context.Response.Headers["X-Frame-Options"] = "DENY"; context.Response.Headers["X-XSS-Protection"] = "1; mode=block"; context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin"; context.Response.Headers["Content-Security-Policy"] = "default-src 'self'"; await next(); }); \\\
A06:2021 — Vulnerable and Outdated Components
Лучшие практики
- dotnet list package --vulnerable
- Dependabot / Renovate
- Pinning версий в .csproj
- SCA инструменты: Snyk, Nexus IQ
A07:2021 — Identification and Authentication Failures
Защита
\\\csharp services.AddIdentity
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15); options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.AllowedForNewUsers = true;
options.User.RequireUniqueEmail = true; options.SignIn.RequireConfirmedEmail = true; });
builder.Services.ConfigureApplicationCookie(options => { options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.Strict; options.ExpireTimeSpan = TimeSpan.FromMinutes(30); options.SlidingExpiration = true; }); \\\
A08:2021 — Software and Data Integrity Failures
Защита
\\\csharp // Anti-forgery (CSRF) builder.Services.AddAntiforgery(options => { options.HeaderName = "X-CSRF-TOKEN"; options.FormFieldName = "__RequestVerificationToken"; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; }); \\\
A09:2021 — Security Logging and Monitoring Failures
Что логировать
- Login attempts (success/failure)
- Authorization failures
- Access to sensitive data
- Configuration changes
- Security events (MFA, password reset)
A10:2021 — Server-Side Request Forgery (SSRF)
Защита
\\\csharp public async Task
if (uri.IsLoopback || uri.Host.StartsWith("10.") || uri.Host.StartsWith("192.168.")) throw new SecurityException("Internal URLs not allowed");
return await _httpClient.GetStringAsync(uri); } \\\
OWASP Security Audit Checklist
- [ ] Все API защищены [Authorize] или глобальной политикой
- [ ] SQL-запросы параметризованы / через EF Core
- [ ] Пароли хешируются (PBKDF2 / Identity)
- [ ] HTTPS enforced в production
- [ ] HSTS, CSP, X-Frame-Options настроены
- [ ] CORS строгая (не *)
- [ ] Нет stack traces в production
- [ ] Зависимости обновлены
- [ ] Sensitive data зашифрована
- [ ] Security logging настроен
Ссылки
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [Microsoft Learn: OWASP Top 10 for .NET](https://learn.microsoft.com/en-us/training/modules/owasp-top-10-for-dotnet-developers/)
- [ASP.NET Core Security](https://learn.microsoft.com/en-us/aspnet/core/security/)
Практика
Authentication и Authorization
# Authentication и Authorization
Введение
Authentication (аутентификация) — проверка идентичности пользователя ("Кто ты?"). Authorization (авторизация) — проверка прав доступа ("Что тебе разрешено?").
В ASP.NET Core эти механизмы реализованы через middleware pipeline и систему политик.
JWT (JSON Web Tokens)
Структура JWT
JWT состоит из трёх частей, разделённых точками: Header.Payload.Signature
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9. <- Header (base64url) eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ. <- Payload (base64url) SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c <- Signature
| Часть | Содержание |
|---|---|
| Header | alg (algorithm), typ (token type), kid (key ID) |
| Payload | iss, aud, exp, nbf, sub, jti, custom claims |
| Signature | HMAC/RS256 signature для integrity |
HS256 vs RS256
| Характеристика | HS256 (HMAC) | RS256 (RSA) |
|---|---|---|
| Ключи | Симметричные (один secret) | Асимметричные (private + public) |
| Распределение | Secret должен быть у всех валидаторов | Public key можно опубликовать |
| Rotation | Требует обновления secret везде | Можно ротировать private key |
| Микросервисы | Не подходит | Идеально |
| Производительность | Быстрее | Медленнее (но приемлемо) |
Реализация RS256
\\\csharp // Генерация RSA ключей using var rsa = RSA.Create(2048); var privateKeyPem = rsa.ExportRSAPrivateKeyPem(); var publicKeyPem = rsa.ExportRSAPublicKeyPem();
// Token Service — подпись public class RsaJwtTokenService : IJwtTokenService { private readonly SigningCredentials _credentials; private readonly JwtOptions _options;
public RsaJwtTokenService(IOptions
public (string token, DateTimeOffset expires) CreateAccessToken( string userId, string userName, IEnumerable
var token = new JwtSecurityToken( issuer: _options.Issuer, audience: _options.Audience, claims: claims, expires: DateTime.UtcNow.AddMinutes(_options.AccessTokenMinutes), signingCredentials: _credentials);
return (new JwtSecurityTokenHandler().WriteToken(token), token.ValidTo); } }
// Валидация — публичный ключ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidateAudience = true, ValidAudience = builder.Configuration["Jwt:Audience"], ValidateIssuerSigningKey = true, IssuerSigningKey = new RsaSecurityKey(publicKey), // публичный ключ RequireExpirationTime = true, ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(30), RoleClaimType = ClaimTypes.Role, ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 } }; }); \\\
Token Rotation
\\\csharp // Refresh Token — long-lived, server-side stored public record RefreshToken( string Token, // hashed value string UserId, DateTime Expires, DateTime Created, DateTime? Revoked = null, string? ReplacedByToken = null, string? ActiveIp = null );
// Rotation при refresh public async Task<(string AccessToken, RefreshToken NewRefreshToken)> RotateAsync( string userId, RefreshToken oldToken, string newIp) { if (oldToken.Expires <= DateTime.UtcNow) throw new SecurityException("Refresh token expired");
if (oldToken.Revoked.HasValue) throw new SecurityException("Refresh token revoked");
// Reuse detection — old token used after replacement if (!string.IsNullOrEmpty(oldToken.ReplacedByToken)) throw new SecurityException("Token reuse detected");
var newToken = new RefreshToken( Token = HashToken(Guid.NewGuid().ToString()), UserId = userId, Expires = DateTime.UtcNow.AddDays(14), Created = DateTime.UtcNow, ActiveIp = newIp );
// Revoke old token await _dbContext.RefreshTokens.AddAsync(oldToken with { Revoked = DateTime.UtcNow, ReplacedByToken = newToken.Token });
return (GenerateAccessToken(userId), newToken); } \\\
OAuth 2.0 и OpenID Connect
OAuth 2.0 Grant Types
| Grant Type | Сценарий | Безопасность |
|---|---|---|
| Authorization Code | Web apps (backend) | Высокая |
| Authorization Code + PKCE | SPAs, mobile apps | Высокая |
| Client Credentials | Service-to-service | Средняя |
| Implicit | Устарел | Низкая |
| Device Code | IoT, TV apps | Средняя |
PKCE (Proof Key for Code Exchange)
PKCE защищает Authorization Code flow от interception атак.
` Client: Authorization Server:
| 1. code_verifier (random) |
| 2. code_challenge = |
| base64url(SHA256( |
| code_verifier)) |
| --- authorize? |
| response_type=code |
| code_challenge=X |
| code_challenge_method=S256 |
| <-- redirect? |
| code=AUTH_CODE |
| --- token? |
| grant_type=authorization_ |
| code=AUTH_CODE |
| code_verifier=ORIGINAL |
| <-- access_token |
`
Реализация OIDC + PKCE в ASP.NET Core
\\\csharp // OIDC для SPA (Backend for Frontend pattern) builder.Services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie() .AddOpenIdConnect(options => { options.Authority = "https://auth.example.com"; options.ClientId = "spa-client"; options.ClientSecret = builder.Configuration["OIDC:ClientSecret"]; options.ResponseType = OpenIdConnectResponseType.Code; options.UsePkce = true; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email"); options.Scope.Add("api://read"); options.Scope.Add("api://write");
options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; options.MapInboundClaims = false; options.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Name; options.TokenValidationParameters.RoleClaimType = "roles"; }); \\\
Cookie-based vs Token-based Authentication
| Характеристика | Cookie | JWT Bearer |
|---|---|---|
| State | Server-side session | Stateless |
| Revocation | Easy (delete session) | Hard (need token blacklist) |
| Scalability | Need shared session store | Horizontal scale |
| Cross-domain | Native | CORS required |
| SPA | BFF pattern recommended | Direct bearer |
| Mobile | Not ideal | Standard |
| CSRF | Need anti-forgery | Not vulnerable |
ASP.NET Core Identity
Базовая настройка
\\\csharp services.AddIdentity
// Lockout options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15); options.Lockout.MaxFailedAccessAttempts = 5;
// User options.User.RequireUniqueEmail = true; options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
// Sign-in options.SignIn.RequireConfirmedEmail = true; }) .AddEntityFrameworkStores
Custom Password Validator
\\\csharp public class CustomPasswordValidator : IPasswordValidator
// Check common passwords if (CommonPasswords.Contains(password.ToLower())) errors.Add(new IdentityError { Code = "WeakPassword", Description = "Password is too common" });
// Check for sequential characters if (HasSequentialChars(password)) errors.Add(new IdentityError { Code = "SequentialPassword", Description = "Password contains sequential characters" });
return errors.Count == 0 ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray()); } } \\\
Практика
- [ ] Реализовать JWT authentication с RS256 signing и token rotation
- [ ] Настроить OAuth 2.0 + OIDC flow для SPA application
- [ ] Создать custom authorization handler с policy-based access control
Ссылки
- [JWT Authentication in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/authentication Jwt)
- [OpenID Connect in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-oidc-web-authentication)
- [ASP.NET Core Identity](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity)
Cryptography в .NET
# Cryptography в .NET
Введение
System.Security.Cryptography — пространство имён .NET, предоставляющее криптографические сервисы.
Symmetric Encryption
AES (Advanced Encryption Standard)
AES — стандарт де-факто для симметричного шифрования. Поддерживает ключи 128, 192, 256 бит.
AES-GCM (Galois/Counter Mode)
GCM — режим аутентифицированного шифрования (AEAD), обеспечивающий конфиденциальность + integrity.
\\\csharp using System.Security.Cryptography;
// AES-GCM шифрование public class AesGcmService { public (byte[] nonce, byte[] tag, byte[] ciphertext) Encrypt(byte[] plaintext, byte[]? aad = null) { using var aes = AesGcm.Create(256); // 32 bytes key
var nonce = new byte[aes.NonceByteSizes.MaxSize]; var tag = new byte[aes.TagByteSizes.MaxSize]; // 16 bytes var ciphertext = new byte[plaintext.Length];
aes.Encrypt(aes.Key, plaintext, ciphertext, tag, aad);
return (nonce, tag, ciphertext); }
public byte[] Decrypt(byte[] plaintext, byte[] nonce, byte[] tag, byte[]? aad = null) { using var aes = AesGcm.Create(256);
var decrypted = new byte[plaintext.Length]; try { aes.Decrypt(nonce, plaintext, tag, decrypted, aad); return decrypted; } catch (AuthenticationTagMismatchException) { throw new SecurityException("Data integrity check failed"); } } }
// Использование var key = RandomNumberGenerator.GetBytes(32); // 256-bit key var service = new AesGcmService(); var data = Encoding.UTF8.GetBytes("Sensitive data"); var (nonce, tag, ciphertext) = service.Encrypt(data);
var decrypted = service.Decrypt(ciphertext, nonce, tag); \\\
ChaCha20-Poly1305
Альтернатива AES-GCM, особенно эффективна на платформах без аппаратной поддержки AES.
\\\csharp using System.Security.Cryptography;
// ChaCha20Poly1305 public class ChaCha20Service { public (byte[] nonce, byte[] tag, byte[] ciphertext) Encrypt(byte[] plaintext, byte[]? aad = null) { using var cipher = new ChaCha20Poly1305(RandomNumberGenerator.GetBytes(32));
var nonce = new byte[cipher.NonceByteSizes.MaxSize]; // 12 bytes var tag = new byte[16]; var ciphertext = new byte[plaintext.Length];
cipher.Encrypt(nonce, plaintext, ciphertext, tag, aad);
return (nonce, tag, ciphertext); } } \\\
Когда использовать
| Алгоритм | Когда использовать |
|---|---|
| AES-GCM | General purpose — основной выбор |
| ChaCha20 | Mobile/IoT, платформы без AES-NI |
| AES-CBC | Legacy only (не рекомендуется для нового кода) |
Asymmetric Encryption
RSA
\\\csharp using System.Security.Cryptography;
// Генерация ключей using var rsa = RSA.Create(2048); // 2048-bit minimum var privateKey = rsa.ExportRSAPrivateKeyPem(); var publicKey = rsa.ExportRSAPublicKeyPem();
// Шифрование (малые данные — для шифрования ключей) var plaintext = RandomNumberGenerator.GetBytes(32); var encrypted = rsa.Encrypt(plaintext, RSAEncryptionPadding.OaepSHA256);
// Подпись var data = Encoding.UTF8.GetBytes("Message to sign"); var signature = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// Верификация bool isValid = rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); \\\
ECC (Elliptic Curve Cryptography)
ECC обеспечивает сопоставимую безопасность с меньшим размером ключа.
\\\csharp using System.Security.Cryptography;
// ECDsa — ECC Digital Signature Algorithm using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); // P-256 var privateKey = ecdsa.ExportECPrivateKeyPem(); var publicKey = ecdsa.ExportSubjectPublicKeyInfoPem();
// Подпись var data = Encoding.UTF8.GetBytes("Message"); var signature = ecdsa.SignData(data, HashAlgorithmName.SHA256);
// Верификация bool isValid = ecdsa.VerifyData(data, signature, HashAlgorithmName.SHA256); \\\
RSA vs ECC
| Характеристика | RSA | ECC (P-256) |
|---|---|---|
| Ключ 128-bit security | 3072 bits | 256 bits |
| Размер подписи | 256 bytes (RSA-2048) | 64 bytes (P-256) |
| Производительность (подпись) | Медленнее | Быстрее |
| Производительность (верификация) | Медленнее | Быстрее |
| Применение | Key exchange, legacy | Modern apps, mobile, JWT |
Hashing
SHA-256
\\\csharp using System.Security.Cryptography;
// Hash данных var data = Encoding.UTF8.GetBytes("input"); var hash = SHA256.HashData(data); var hex = Convert.ToHexString(hash); // "A1B2C3..."
// HMAC (Hash-based Message Authentication Code) var key = RandomNumberGenerator.GetBytes(32); var hmac = new HMACSHA256(key); var mac = hmac.ComputeHash(data); \\\
Password Hashing
| Алгоритм | Безопасность | Рекомендация |
|---|---|---|
| MD5 | Устарел | Никогда |
| SHA-256 (plain) | Слабый | Никогда |
| PBKDF2 | Средний | Acceptable |
| bcrypt | Хороший | Recommended |
| Argon2id | Лучший | Strongly recommended |
Argon2id — .NET
Argon2id — победитель Password Hashing Competition 2015.
\\\csharp using System.Security.Cryptography;
public class Argon2idService { private const int SaltSize = 16; private const int HashSize = 32; private const int Iterations = 3; private const int MemorySize = 65536; // 64 MB private const int Parallelism = 4;
public string HashPassword(string password) { var salt = RandomNumberGenerator.GetBytes(SaltSize); var hash = new byte[HashSize];
using var argon2 = new Argon2id(salt) { Iterations = Iterations, MemorySize = MemorySize, Parallelism = Parallelism, HashSize = HashSize };
argon2.GetBytes(hash);
// Format: \\=19\=65536,t=3,p=4\$
public bool VerifyPassword(string password, string stored) { var parts = stored.Split(':'); var salt = Convert.FromBase64String(parts[0]); var expectedHash = Convert.FromBase64String(parts[1]);
var actualHash = new byte[HashSize]; using var argon2 = new Argon2id(salt) { Iterations = Iterations, MemorySize = MemorySize, Parallelism = Parallelism, HashSize = HashSize };
argon2.GetBytes(actualHash);
// Constant-time comparison return CryptographicOperations.FixedTimeEquals(actualHash, expectedHash); } } \\\
HMAC-based Message Authentication
\\\csharp public class HmacSignatureService { private readonly HMACSHA256 _hmac;
public HmacSignatureService(byte[] key) { _hmac = new HMACSHA256(key); }
public string Sign(string message) { var hash = _hmac.ComputeHash(Encoding.UTF8.GetBytes(message)); return Convert.ToHexString(hash); }
public bool Verify(string message, string signature) { var expected = Sign(message); return CryptographicOperations.FixedTimeEquals( Convert.FromHexString(signature), Convert.FromHexString(expected)); } }
// API request signing public async Task
var signature = new HmacSignatureService(signingKey).Sign(payload);
var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = new StringContent(body, Encoding.UTF8, "application/json") }; request.Headers.Add("X-Signature", signature); request.Headers.Add("X-Timestamp", timestamp.ToString());
return await client.SendAsync(request); } \\\
Best Practices
- Никогда не используйте MD5, SHA-1, DES, RC4 для нового кода
- AES-GCM — основной выбор для симметричного шифрования
- ECC предпочтительнее RSA для нового кода
- Argon2id или bcrypt для password hashing
- Constant-time comparison для sensitive data (CryptographicOperations.FixedTimeEquals)
- Unique nonce для каждой операции шифрования
- Key rotation — регулярно обновляйте ключи
- Data Protection API для прикладного шифрования в .NET
Практика
- [ ] Реализовать secure password storage с Argon2id
- [ ] Создать encrypted file storage service с AES-GCM
- [ ] Написать HMAC-based message authentication для API requests
Ссылки
- [System.Security.Cryptography Documentation](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography)
- [AesGcm Class](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.aesgcm)
- [ASP.NET Core Data Protection](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection)
Authorization Patterns
# Authorization Patterns
Введение
ASP.NET Core поддерживает несколько паттернов авторизации. Выбор зависит от требований приложения.
RBAC (Role-Based Access Control)
Теория
Доступ определяется ролями пользователя. Каждая роль содержит набор разрешений.
` User --> Role --> Permissions
Admin: [Users.Read, Users.Write, Users.Delete, Reports.View] Manager: [Users.Read, Reports.View, Reports.Export] Viewer: [Reports.View] `
Реализация
\\\csharp // Регистрация ролей и пользователей (ASP.NET Core Identity) var roles = new[] { "Admin", "Manager", "Viewer" }; foreach (var role in roles) { if (!await _roleManager.RoleExistsAsync(role)) await _roleManager.CreateAsync(new IdentityRole(role)); }
// Использование в контроллере [Authorize(Roles = "Admin,Manager")] public IActionResult AdminPanel() => Ok();
[Authorize(Roles = "Admin")] [HttpDelete("/users/{id}")] public IActionResult DeleteUser(int id) => Ok(); \\\
Hierarchical Roles
\\\csharp // Hierarchy: SuperAdmin > Admin > Manager > Viewer public class RoleHierarchyService { private readonly Dictionary
public bool HasPermission(string userRole, string requiredRole) { return _hierarchy.TryGetValue(userRole, out var allowed) && allowed.Contains(requiredRole); } }
// Policy-based с иерархией builder.Services.AddAuthorization(options => { options.AddPolicy("RequireAdminOrAbove", policy => policy.Requirements.Add(new RoleHierarchyRequirement("Admin"))); });
public class RoleHierarchyRequirement : IAuthorizationRequirement { public string MinRole { get; } public RoleHierarchyRequirement(string minRole) => MinRole = minRole; }
public class RoleHierarchyHandler : AuthorizationHandler
public RoleHierarchyHandler(RoleHierarchyService hierarchy) => _hierarchy = hierarchy;
protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, RoleHierarchyRequirement requirement) { var userRoles = context.User.Claims .Where(c => c.Type == ClaimTypes.Role) .Select(c => c.Value) .ToList();
if (userRoles.Any(r => _hierarchy.HasPermission(r, requirement.MinRole))) context.Succeed(requirement);
return Task.CompletedTask; } } \\\
ABAC (Attribute-Based Access Control)
Теория
Доступ определяется атрибутами пользователя, ресурса, действия и контекста.
IF user.department == resource.department AND user.level >= resource.sensitivity AND time is business_hours THEN allow
Реализация
\\\csharp // ABAC Policy Engine public class AbacPolicyEngine { public async Task
// Resource attributes var resourceDept = resource.Metadata.GetAttribute("department"); var sensitivity = int.Parse(resource.Metadata.GetAttribute("sensitivity") ?? "0");
// Context attributes var ip = context.Connection.RemoteIpAddress?.ToString(); var isInternal = IsInternalIp(ip); var time = DateTime.UtcNow;
// Policy rules var rules = await LoadPoliciesAsync(resource.Type, action);
foreach (var rule in rules) { if (!EvaluateRule(rule, department, level, team, resourceDept, sensitivity, ip, isInternal, time)) return false; }
return true; }
private bool EvaluateRule(PolicyRule rule, ...) { return rule.Condition switch { "department_match" => department == resourceDept, "level_gte" => level >= sensitivity, "internal_only" => isInternal, "business_hours" => time.Hour is >= 8 and <= 18 && time.DayOfWeek is not (DayOfWeek.Saturday or DayOfWeek.Sunday), _ => false }; } }
// Регистрация builder.Services.AddSingleton
public class AbacRequirement : IAuthorizationRequirement { }
public class AbacHandler : AuthorizationHandler
public AbacHandler(AbacPolicyEngine engine) => _engine = engine;
protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, AbacRequirement requirement) { if (context.Resource is HttpContext httpContext) { var resource = GetResourceFromContext(httpContext); var action = httpContext.Request.Method;
if (await _engine.EvaluateAsync(context.User, resource, action, httpContext)) context.Succeed(requirement); } } } \\\
PBAC (Policy-Based Access Control)
Теория
Отдельный policy engine (OPA, Cedar) управляет политиками независимо от приложения.
OPA (Open Policy Agent) интеграция
\\\csharp // OPA Policy as Code (Rego) /* package netapp.authorization
import future rego.v1
default allow = false
allow { input.user.role == "admin" }
allow { input.user.department == input.resource.department input.user.level >= input.resource.sensitivity_level }
allow { input.action == "read" input.resource.public == true } */
public class OpaAuthorizationService { private readonly HttpClient _opaClient; private readonly string _policyUrl;
public OpaAuthorizationService(HttpClient opaClient, IConfiguration config) { _opaClient = opaClient; _policyUrl = config["OPA:Url"]; }
public async Task
var response = await _opaClient.PostAsJsonAsync( $"{_policyUrl}/v1/data/netapp/authorization", input);
var result = await response.Content.ReadFromJsonAsync
public record OpaResult(bool Allow); \\\
Resource-Based Authorization
Теория
Доступ зависит от конкретного ресурса. Например, только автор документа может его редактировать.
\\\csharp // Requirement public class DocumentAccessRequirement : IAuthorizationRequirement { public string Action { get; } public DocumentAccessRequirement(string action) => Action = action; }
// Handler public class DocumentAuthorizationHandler : AuthorizationHandler
public DocumentAuthorizationHandler(IUserService userService) => _userService = userService;
protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, DocumentAccessRequirement requirement, Document resource) { if (resource == null) { context.Fail(); return; }
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
switch (requirement.Action) { case "Read": // Anyone with access can read if (resource.IsPublic || await _userService.CanAccessDocument(userId, resource.Id)) context.Succeed(requirement); break;
case "Edit": // Only owner or editor if (resource.OwnerId == userId || await _userService.IsEditor(userId, resource.Id)) context.Succeed(requirement); break;
case "Delete": // Only owner if (resource.OwnerId == userId) context.Succeed(requirement); break; } } }
// Usage in controller [Authorize(Policy = "DocumentAccess")] [HttpPut("/documents/{id}")] public async Task
var result = await _authorizationService.AuthorizeAsync(User, doc, "DocumentAccess"); if (!result.Succeeded) return Forbid();
// Update logic return Ok(); } \\\
Permission-Based Authorization (Dynamic)
Теория
Права хранятся в БД и могут меняться без перезапуска приложения.
\\\csharp // Database model public class RolePermission { public int RoleId { get; set; } public string Permission { get; set; } // "Users.Read", "Orders.Write" }
// Requirement public class PermissionRequirement : IAuthorizationRequirement { public string Permission { get; } public PermissionRequirement(string permission) => Permission = permission; }
// Handler — динамическая проверка из БД public class PermissionHandler : AuthorizationHandler
public PermissionHandler(IServiceProvider services) => _services = services;
protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, PermissionRequirement requirement) { using var scope = _services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); var roles = context.User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList();
var userPermissions = await db.RolePermissions .Where(rp => roles.Contains(rp.RoleId.ToString())) .Select(rp => rp.Permission) .ToListAsync();
if (userPermissions.Contains(requirement.Permission)) context.Succeed(requirement); } }
// Dynamic policy provider public class CustomAuthorizationPolicyProvider : IAuthorizationPolicyProvider { private readonly DefaultAuthorizationPolicyProvider _fallback;
public CustomAuthorizationPolicyProvider(IOptions
public string PolicyName => _fallback.PolicyName;
public Task
// ... other interface members }
// Registration builder.Services.AddSingleton
Сравнение паттернов
| Паттерн | Сложность | Гибкость | Сценарий |
|---|---|---|---|
| RBAC | Низкая | Средняя | Корпоративные приложения |
| ABAC | Высокая | Высокая | Enterprise, multi-tenant |
| PBAC | Средняя | Очень высокая | Microservices, distributed |
| Resource-based | Средняя | Высокая | Document management, CMS |
| Permission-based | Средняя | Высокая | SaaS с динамическими правами |
Практика
- [ ] Реализовать RBAC system с hierarchical roles и permission inheritance
- [ ] Создать ABAC policy engine для resource-level access control
- [ ] Написать custom IAuthorizationHandler для complex business rules
Ссылки
- [Policy-Based Authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies)
- [Resource-Based Authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased)
- [Custom Authorization with IAuthorizationRequirementData](https://learn.microsoft.com/en-us/aspnet/core/mvc/security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata-in-mvc)
API Security
# API Security
Введение
API Security охватывает protection механизмы, специфичные для веб-API: rate limiting, CORS, CSRF, input validation, output encoding.
Rate Limiting
Алгоритмы
| Алгоритм | Описание | Сценарий |
|---|---|---|
| Fixed Window | Счётчик за фиксированный период | Простые сценарии |
| Sliding Window | Плавное ограничение | Более точное |
| Token Bucket | Burst + average rate | API с burst traffic |
| Sliding Log | Каждая запись времени | Максимальная точность |
Встроенный Rate Limiter (.NET 8+)
\\\csharp // In-memory rate limiting (built-in) builder.Services.AddRateLimiter(options => { // Per-IP fixed window options.AddPolicy("FixedWindow", context => RateLimitPartition.GetFixedWindowLimiter( context.Connection.RemoteIpAddress?.ToString() ?? "anon", _ => new FixedWindowRateLimiterOptions { PermitLimit = 100, Window = TimeSpan.FromMinutes(1) }));
// Per-user token bucket options.AddPolicy("TokenBucket", context => { var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); return RateLimitPartition.GetTokenBucketLimiter( userId ?? "anon", _ => new TokenBucketRateLimiterOptions { TokenLimit = 50, TokensPerPeriod = 10, ReplenishmentPeriod = TimeSpan.FromSeconds(5), QueueLimit = 5 })); });
options.OnRejected = async (context, _) => { context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; context.HttpContext.Response.Headers.RetryAfter = "60"; await context.HttpContext.Response.WriteAsJsonAsync(new { error = "Rate limit exceeded", retryAfter = 60 }); }; });
app.UseRateLimiter();
// Применение к endpoint builder.MapGet("/api/data", async () => "Data") .WithMetadata(new EnableRateLimitingMetadata("FixedWindow")); \\\
Distributed Rate Limiter с Redis
\\\csharp // Redis-backed rate limiter (Lua script for atomicity) public class RedisRateLimiter { private readonly IDatabase _redis; private readonly string _luaScript = @" local key = KEYS[1] local limit = tonumber(ARGV[1]) local window = tonumber(ARGV[2])
local current = redis.call('INCR', key) if current == 1 then redis.call('EXPIRE', key, window) end
if current > limit then return redis.call('TTL', key) end
return 0 ";
public RedisRateLimiter(IConnectionMultiplexer redis) => _redis = redis.GetDatabase();
public async Task
var remaining = Math.Max(0, limit - (int)result);
return new RateLimitResult { Allowed = (int)result <= limit, Remaining = remaining, ResetAt = DateTimeOffset.UtcNow.Add(window) }; } }
// Middleware public class RateLimitMiddleware { private readonly RequestDelegate _next; private readonly RedisRateLimiter _limiter;
public RateLimitMiddleware(RequestDelegate next, RedisRateLimiter limiter) { _next = next; _limiter = limiter; }
public async Task InvokeAsync(HttpContext context) { var clientId = context.Connection.RemoteIpAddress?.ToString() ?? "anon"; var path = context.Request.Path.Value ?? "/"; var key = $"{clientId}:{path}";
var result = await _limiter.CheckAsync(key, limit: 100, TimeSpan.FromMinutes(1));
context.Response.Headers["X-RateLimit-Limit"] = "100"; context.Response.Headers["X-RateLimit-Remaining"] = result.Remaining.ToString(); context.Response.Headers["X-RateLimit-Reset"] = result.ResetAt.ToUnixTimeSeconds().ToString();
if (!result.Allowed) { context.Response.StatusCode = StatusCodes.Status429TooManyRequests; return; }
await _next(context); } } \\\
CORS (Cross-Origin Resource Sharing)
Правильная конфигурация
\\\csharp // Строгая CORS политика builder.Services.AddCors(options => { options.AddPolicy("SecureApi", policy => { policy.WithOrigins( "https://app.example.com", "https://admin.example.com") .WithMethods("GET", "POST", "PUT", "DELETE") .WithHeaders( HeaderNames.ContentType, HeaderNames.Authorization, "X-CSRF-TOKEN") .AllowCredentials() .WithExposedHeaders( "X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset"); });
// Default — block all options.AddPolicy("Default", policy => policy.DenyAll()); });
app.UseCors("SecureApi");
// Per-endpoint CORS builder.MapGet("/api/public", () => "Public") .RequireCors("PublicPolicy"); \\\
Частые ошибки
- AllowAnyOrigin() + AllowCredentials() — конфликт (browser rejects)
- AllowAnyHeader() — unnecessary broad access
- Missing Origin validation — SSRF vector
CSRF Protection
SameSite Cookies
\\\csharp // Global SameSite configuration builder.Services.Configure
builder.Services.ConfigureApplicationCookie(options => { options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.HttpOnly = true; }); \\\
Anti-Forgery Tokens (MVC/Razor)
\\\csharp // Server [HttpPost] [ValidateAntiForgeryToken] public IActionResult Submit(SubmitModel model) { ... }
// Client (Razor)
// API с cookies — custom header builder.Services.AddAntiforgery(options => { options.HeaderName = "X-CSRF-TOKEN"; options.FormFieldName = "__RequestVerificationToken"; });
// Minimal API app.MapPost("/api/data", [Authorize, ValidateAntiForgeryToken](DataRequest req) => ...); \\\
Input Validation
FluentValidation
\\\csharp // Validator public class CreateUserValidator : AbstractValidator
RuleFor(x => x.Password) .NotEmpty() .MinimumLength(12).WithMessage("Password must be at least 12 chars") .Matches(@"[A-Z]").WithMessage("Must contain uppercase") .Matches(@"[a-z]").WithMessage("Must contain lowercase") .Matches(@"[0-9]").WithMessage("Must contain digit") .Matches(@"[^a-zA-Z0-9]").WithMessage("Must contain special char");
RuleFor(x => x.Age) .InclusiveBetween(18, 120).WithMessage("Age must be 18-120");
RuleFor(x => x.Phone) .Matches(@"^\+?[1-9]\d{1,14}$").WithMessage("Invalid phone format"); } }
// Registration builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
// Auto-validation in Minimal API app.MapPost("/users", ([FromBody, Validate] CreateUserRequest req) => ...);
// Controller with FluentValidation public class UsersController : ControllerBase { private readonly IValidator
public UsersController(IValidator
[HttpPost] public IActionResult Create([FromBody] CreateUserRequest request) { var result = _validator.Validate(request); if (!result.IsValid) return BadRequest(new { errors = result.Errors });
// Process return Created(); } } \\\
Deep Validation
\\\csharp public class OrderRequest { [Required] public List
public class OrderItemRequest { [Required] public string ProductId { get; set; }
[Range(1, 1000)] public int Quantity { get; set; }
[NestedValidation] public ShippingInfoRequest Shipping { get; set; } }
// Nested validation public class ShippingInfoRequest { [Required] public string Address { get; set; }
[Required] [ZipCode] public string ZipCode { get; set; } } \\\
Output Encoding (XSS Prevention)
Защита от XSS
\\\csharp // ASP.NET Core MVC — auto-encoding by default @Model.UserInput // HTML encoded automatically
// Raw output — dangerous! @Html.Raw(Model.UserInput) // Only if absolutely necessary
// JSON API — no XSS (browser parses JSON, not HTML) [HttpGet("/api/data")] public IActionResult GetData() => Json(new { message = userInput });
// Content Security Policy app.Use(async (context, next) => { context.Response.Headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; " + "base-uri 'self'; form-action 'self'"; await next(); }); \\\
Security Headers
\\\csharp app.Use(async (context, next) => { // HSTS context.Response.Headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload";
// XSS Protection context.Response.Headers["X-XSS-Protection"] = "1; mode=block";
// Clickjacking context.Response.Headers["X-Frame-Options"] = "DENY";
// MIME sniffing context.Response.Headers["X-Content-Type-Options"] = "nosniff";
// Referrer policy context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
// Permissions Policy context.Response.Headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()";
// Remove server headers context.Response.Headers.Remove("Server");
await next(); }); \\\
Практика
- [ ] Реализовать distributed rate limiter с Redis backend
- [ ] Настроить secure CORS policy для multi-origin environment
- [ ] Создать comprehensive input validation pipeline с custom validators
Ссылки
- [ASP.NET Core Rate Limiting](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/rate-limit)
- [ASP.NET Core CORS](https://learn.microsoft.com/en-us/aspnet/core/security/cors)
- [FluentValidation](https://docs.fluentvalidation.net/en/latest/)
Secrets Management
Введение
Secrets Management - практика безопасного хранения и доступа к чувствительным данным: password, API keys, connection strings, certificates.
Key principle: secrets never in source code, never in appsettings.json in production.
IConfiguration Hierarchy
1. AppHost (ASPNETCORE_*)
2. Command line arguments
3. Environment variables (APPSETTING_*)
4. appsettings.{Environment}.json
5. appsettings.json
6. User Secrets (Development only)
7. Azure Key Vault (if configured)
8. Azure App Service configurationUser Secrets (Development)
dotnet user-secrets init
dotnet user-secrets set "Jwt:Key" "my-secret-key"
dotnet user-secrets set "ConnectionStrings:Default" "Server=..."// Program.cs - User Secrets loaded automatically in Development
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddUserSecrets<Program>(optional: true);Environment Variables
# Linux
export ConnectionStrings__Default="Server=..."
export Jwt__Issuer="https://auth.example.com"
# Windows PowerShell
$env:ConnectionStrings__Default = "Server=..."
$env:Jwt__Issuer = "https://auth.example.com"Azure Key Vault Integration
Secret Client
// Install: Azure.Identity, Azure.Security.KeyVault.Secrets
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
// DefaultAzureCredential - auto-detects auth method
// Development: Azure CLI login
// Production: Managed Identity
var credential = new DefaultAzureCredential();
var vaultUri = new Uri("https://myvault.vault.azure.net/");
var client = new SecretClient(vaultUri, credential, new SecretClientOptions
{
Retry = { Delay = TimeSpan.FromSeconds(2), MaxDelay = TimeSpan.FromSeconds(16), MaxRetries = 5, Mode = RetryMode.Exponential }
});
// Set secret
KeyVaultSecret secret = client.SetSecret("db-connection-string", "Server=prod;Database=app;");
// Get secret
KeyVaultSecret retrieved = client.GetSecret("db-connection-string");
string value = retrieved.Value;
// List secrets
await foreach (var prop in client.GetPropertiesOfSecretsAsync())
{
Console.WriteLine($"{prop.Name} - {prop.CreatedOn}");
}Configuration Provider
// Key Vault as configuration source
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{vaultName}.vault.azure.net/"),
new DefaultAzureCredential());
// Now accessible via standard IConfiguration
var connectionString = builder.Configuration["db-connection-string"];
var apiKey = builder.Configuration["api-key"];Managed Identity Setup
# Enable system-assigned managed identity for App Service
az webapp identity assign --name myapp --resource-group myrg
# Grant Key Vault access
az role assignment create --role "Key Vault Secrets User" \
--assignee <principal-id> \
--scope /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.KeyVault/vaults/myvaultCertificate Management
Certificate Storage
// Load certificate from Key Vault
var certClient = new CertificateClient(new Uri($"https://{vaultName}.vault.azure.net/"), credential);
var certOperation = await certClient.StartCreateCertificateAsync("my-cert", CertPolicy.CreateSelfSigned("CN=app.example.com"));
await certOperation.WaitForCompletionAsync();
// Load certificate for HTTPS
var cert = new X509Certificate2(certBundle.Certificate);
builder.WebHost.UseHttps(cert);
// Certificate rotation
public class CertificateRotationService : BackgroundService
{
private readonly CertificateClient _client;
private readonly string _certName;
public CertificateRotationService(CertificateClient client, IConfiguration config)
{
_client = client;
_certName = config["CertificateName"]!;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var certProps = await _client.GetCertificateAsync(_certName, stoppingToken);
var expiry = certProps.Properties.ExpiresOn;
// Rotate 30 days before expiry
if (expiry - DateTime.UtcNow < TimeSpan.FromDays(30))
{
await _client.StartReloadCertificateAsync(_certName, stoppingToken);
_logger.LogInformation("Certificate {Name} rotated", _certName);
}
var nextCheck = expiry - DateTime.UtcNow - TimeSpan.FromDays(29);
await Task.Delay(nextCheck, stoppingToken);
}
}
}Connection String Encryption
Data Protection API
// Encrypt connection string
services.AddDataProtection()
.SetApplicationName("SecureApp")
.PersistKeysToAzureBlobStorage("DefaultEndpointsProtocol=https;") // Azure Blob storage
.ProtectKeysWithAzureKeyVault(new Uri("https://myvault.vault.azure.net/keys/my-protection-key"), new DefaultAzureCredential());
// Usage
public class EncryptedConnectionStringService
{
private readonly IDataProtectionProvider _dp;
public EncryptedConnectionStringService(IDataProtectionProvider provider)
=> _dp = provider;
public string DecryptConnectionString(string encrypted)
{
var protector = _dp.CreateProtector("ConnectionString");
return protector.Unprotect(encrypted);
}
}DPAPI (Windows) / DPAPI-Ng (Linux/macOS)
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/secure/key-storage"))
.ProtectKeysWithCryptographicUser(); // Windows DPAPIZero-Trust Principles
Never Trust, Always Verify
// Internal service-to-service auth
public class ZeroTrustMiddleware
{
private readonly RequestDelegate _next;
private readonly IHttpClientFactory _httpClient;
public ZeroTrustMiddleware(RequestDelegate next, IHttpClientFactory httpClient)
{
_next = next;
_httpClient = httpClient;
}
public async Task InvokeAsync(HttpContext context)
{
// Verify mTLS certificate
var clientCert = context.Connection.ClientCertificate;
if (clientCert == null)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
// Verify certificate chain
var isValid = await VerifyCertificateAsync(clientCert);
if (!isValid)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
// Verify request signature
var signature = context.Request.Headers["X-Request-Signature"];
if (string.IsNullOrEmpty(signature))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
await _next(context);
}
}mTLS (Mutual TLS)
// Server-side mTLS
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenLocalhost(5001, listenOptions =>
{
listenOptions.UseHttps(httpsOptions =>
{
httpsOptions.ServerCertificate = LoadServerCert();
httpsOptions.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
});
});
// Client-side mTLS
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(LoadClientCert());
var client = new HttpClient(handler);Secrets Audit Trail
// Log all secret access
public class SecretAccessLogger
{
private readonly ILogger<SecretAccessLogger> _logger;
public SecretAccessLogger(ILogger<SecretAccessLogger> logger) => _logger = logger;
public void LogSecretAccess(string secretName, string caller, string ipAddress)
{
_logger.LogWarning("Secret accessed: {SecretName} by {Caller} from {Ip}",
secretName, caller, ipAddress);
}
}
// Integration with Key Vault access logging
// Enable auditing in Azure Portal -> Key Vault -> Diagnostic Settings
// Route to Log Analytics / Event Hub / Storage AccountBest Practices
- Никогда не хранить secrets в коде или git
- Use Managed Identity - без client secrets
- Rotate secrets regularly (90 days recommended)
- Least privilege - minimum permissions для каждого сервиса
- Encrypt secrets at rest и in transit
- Audit all secret access
- Use Key Vault references в App Service - no code changes needed
- Backup Key Vault before major changes
Практика
- [ ] Настроить Azure Key Vault integration с managed identity
- [ ] Реализовать automatic certificate rotation mechanism
- [ ] Создать secrets audit trail с access logging
Ссылки
- [Azure Key Vault with .NET](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/security.keyvault.secrets-readme)
- [Key Vault Configuration Provider](https://learn.microsoft.com/en-us/aspnet/core/security/key-vault-configuration)
- [ASP.NET Core Data Protection](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection)
Security Architecture
Введение
Security Architecture — проектирование систем с учётом безопасности на архитектурном уровне. Не "добавить безопасность потом", а "secure by design".
Zero Trust Architecture
Principles
- Never trust, always verify — ни один запрос не доверяется автоматически
- Least privilege access — минимальные права для каждого запроса
- Assume breach — проектировать как будто атака уже происходит
Zero Trust Components
┌─────────────────────────────────────────────────┐
│ Zero Trust Boundary │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Identity │ │ Device │ │ App/ │ │
│ │ & MFA │ │ Health │ │ Data │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └───────────────┼───────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Policy Engine │ │
│ │ (PDP/PEP) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Access Gateway │ │
│ │ (ZTNA) │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────┘Implementation in ASP.NET Core
// Zero Trust middleware pipeline
public class ZeroTrustMiddleware
{
private readonly RequestDelegate _next;
private readonly IAuthorizationService _auth;
public ZeroTrustMiddleware(
RequestDelegate next,
IAuthorizationService auth)
{
_next = next;
_auth = auth;
}
public async Task InvokeAsync(HttpContext context)
{
// 1. Verify identity
if (!context.User.Identity?.IsAuthenticated == true)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
// 2. Verify device health (certificate, platform)
var clientCert = context.Connection.ClientCertificate;
if (clientCert == null)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}
// 3. Verify request integrity
var timestamp = context.Request.Headers["X-Timestamp"].ToString();
if (string.IsNullOrEmpty(timestamp))
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
var ts = DateTimeOffset.FromUnixTimeSeconds(long.Parse(timestamp));
if (DateTime.UtcNow - ts > TimeSpan.FromMinutes(5))
{
context.Response.StatusCode = StatusCodes.Status408RequestTimeout;
return;
}
// 4. Policy evaluation
var result = await _auth.AuthorizeAsync(
context.User,
context.Request.Path.ToString(),
"ZeroTrustPolicy");
if (!result.Succeeded)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}
await _next(context);
}
}Defense in Depth
Multiple Security Layers
Layer 1: Network Security (Firewall, WAF, DDoS protection)
Layer 2: Identity (Authentication, MFA, SSO)
Layer 3: Application (Input validation, Authorization, CSP)
Layer 4: Data (Encryption, Tokenization, DLP)
Layer 5: Host (OS hardening, AV, EDR)
Layer 6: Monitoring (SIEM, logging, alerting)Implementation
// Layer 1: Network - HTTPS enforcement + HSTS
app.UseHttpsRedirection();
app.UseHsts();
// Layer 2: Identity - Authentication
app.UseAuthentication();
// Layer 3: Application - Authorization
app.UseAuthorization();
// Layer 3: Application - Security headers
app.Use(async (context, next) =>
{
context.Response.Headers["X-Content-Type-Options"] = "nosniff";
context.Response.Headers["X-Frame-Options"] = "DENY";
context.Response.Headers["Content-Security-Policy"] = "default-src 'self'";
await next();
});
// Layer 3: Application - Input validation
app.Use(async (context, next) =>
{
// Request size limit
context.Request.EnableBuffering();
var maxRequestBodySize = 10 * 1024 * 1024; // 10MB
if (context.Request.ContentLength > maxRequestBodySize)
{
context.Response.StatusCode = StatusCodes.Status413PayloadTooLarge;
return;
}
await next();
});
// Layer 6: Monitoring - Request logging
app.Use(async (context, next) =>
{
var sw = System.Diagnostics.Stopwatch.StartNew();
await next();
sw.Stop();
// Log security-relevant metrics
if (context.Response.StatusCode >= 400)
{
// Log errors for security review
}
if (sw.Elapsed > TimeSpan.FromSeconds(5))
{
// Log slow requests (potential DoS)
}
});Security by Design
Threat Modeling (STRIDE)
| Threat | Question | Example |
|---|---|---|
| Spoofing | Can someone impersonate a user/service? | Stolen JWT token |
| Tampering | Can data be modified in transit? | Man-in-the-middle |
| Repudiation | Can someone deny an action? | No audit log |
| Information Disclosure | Can sensitive data be leaked? | Error message with stack trace |
| DoS | Can the service be made unavailable? | Unbounded request loop |
| Elevation of Privilege | Can a user gain higher access? | IDOR, missing authorization |
Threat Modeling Template
1. Identify assets (data, services, infrastructure)
2. Create data flow diagrams
3. Identify trust boundaries
4. Apply STRIDE to each component
5. Identify mitigations
6. Validate assumptionsExample: STRIDE for E-Commerce API
Component: Order API
Trust Boundary: Public -> API Gateway -> Order Service -> Database
STRIDE Analysis:
┌────────────┬──────────────────────────────────────────┐
│ Threat │ Mitigation │
├────────────┼──────────────────────────────────────────┤
│ Spoofing │ JWT authentication, mTLS internal │
│ Tampering │ TLS, request signing, input validation │
│ Repudiation│ Audit logging, idempotency keys │
│ Info Disc. │ Response filtering, error handling │
│ DoS │ Rate limiting, request timeouts │
│ Elevation │ Policy-based authorization, ownership │
└────────────┴──────────────────────────────────────────┘Secure SDLC
Security Gates in CI/CD
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Code │ │ Build │ │ Test │ │ Deploy │
│ Review │ -> │ Scan │ -> │ (SAST) │ -> │ (DAST) │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │
Manual PR Dependency Unit + Container
checks audit Integration image scan
(Dependabot) tests (Trivy)GitHub Actions Example
name: Security Pipeline
on: [push, pull_request]
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run SAST scan
uses: github/codeql-action/init@v3
with:
languages: csharp
- name: Run CodeQL analysis
uses: github/codeql-action/analyze@v3
dependency-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
- name: Audit dependencies
run: dotnet list package --vulnerable --include-transitive
container-scan:
runs-on: ubuntu-latest
steps:
- name: Build and scan image
run: |
docker build -t myapp:${{ github.sha }} .
trivy image --severity HIGH,CRITICAL myapp:${{ github.sha }}Compliance Frameworks
GDPR Basics
| Requirement | Implementation |
|---|---|
| Data minimization | Collect only necessary data |
| Right to be forgotten | Delete user data on request |
| Data portability | Export user data in standard format |
| Consent management | Track and manage user consent |
| Breach notification | Alert within 72 hours |
SOC 2
| Trust Service Criterion | Technical Controls |
|---|---|
| Security | Auth, encryption, monitoring |
| Availability | SLA monitoring, backups, DR |
| Processing Integrity | Data validation, error handling |
| Confidentiality | NDA, access controls, encryption |
| Privacy | Data handling, consent management |
ISO 27001
| Control Area | Technical Implementation |
|---|---|
| A.9 Access Control | RBAC, MFA, session management |
| A.10 Cryptography | TLS, encryption at rest, key management |
| A.12 Operations | Logging, monitoring, backup |
| A.14 System Security | Secure SDLC, vulnerability management |
| A.16 Incident Mgmt | SIEM, alerting, playbook |
Security Checklist for Architecture Review
- [ ] Threat modeling completed for all critical components
- [ ] Trust boundaries documented
- [ ] Authentication strategy defined (MFA for privileged access)
- [ ] Authorization model selected (RBAC/ABAC/PBAC)
- [ ] Encryption strategy (at rest + in transit)
- [ ] Secrets management approach (Key Vault, etc.)
- [ ] Logging and monitoring strategy
- [ ] Incident response plan documented
- [ ] Compliance requirements identified
- [ ] CI/CD security gates configured
- [ ] Penetration testing scheduled
- [ ] Security monitoring and alerting defined
Практика
- [ ] Провести threat modeling session для critical system component
- [ ] Создать security checklist для code review process
- [ ] Настроить automated vulnerability scanning в CI pipeline (Dependabot, Snyk)
Ссылки
- [Microsoft Cloud Security Benchmark](https://learn.microsoft.com/en-us/azure/security/cloud-security-best-practices)
- [OWASP Threat Modeling](https://owasp.org/www-community/Application_Threat_Modeling)
- [NIST Cybersecurity Framework](https://www.nist.gov/cyberframework)
Advanced Authentication Scenarios
Введение
Продвинутые сценарии аутентификации: MFA, federated identity, API Key management, service-to-service auth.
Multi-Factor Authentication (MFA)
TOTP (Time-based One-Time Password)
RFC 6238 — стандарт для TOTP (Google Authenticator, Authy и т.д.)
using OtpNet;
public class TotpService
{
private const int SecretKeySize = 20; // 160 bits
private const int CodeDigits = 6;
private const int PeriodSeconds = 30;
public string GenerateSecretKey()
{
var key = new byte[SecretKeySize];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(key);
return Base32.Encode(key);
}
public string GenerateUri(string secretKey, string issuer, string account)
{
return $"otpauth://totp/{Uri.EscapeDataString(issuer)}:{Uri.EscapeDataString(account)}" +
$"?secret={secretKey}&issuer={Uri.EscapeDataString(issuer)}" +
$"&digits={CodeDigits}&period={PeriodSeconds}";
}
public bool VerifyCode(string secretKey, string code)
{
var key = Base32.Decode(secretKey);
var totp = new Totp(key, CodeDigits, PeriodSeconds);
// Allow 30-second window before and after (60 seconds total)
var unixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var timeStep = (long)(unixTime / PeriodSeconds);
for (int i = -1; i <= 1; i++)
{
if (totp.VerifyTotp(unixTime + (i * PeriodSeconds), code, out _))
return true;
}
return false;
}
}Backup Codes
public class BackupCodeService
{
private const int CodeCount = 10;
private const int CodeLength = 8;
public List<string> GenerateBackupCodes()
{
var codes = new List<string>();
for (int i = 0; i < CodeCount; i++)
{
var code = GenerateRandomCode();
var hashed = HashCode(code);
codes.Add(hashed); // Store hashed codes only
}
return codes;
}
public bool ConsumeCode(string storedHash, string inputCode)
{
var inputHash = HashCode(inputCode);
if (inputHash == storedHash)
{
// Code consumed - mark as used (set to null or delete)
return true;
}
return false;
}
private string GenerateRandomCode()
{
const string chars = "0123456789";
return new string(Enumerable.Repeat(chars, CodeLength)
.Select(s => s[RandomNumberGenerator.GetInt32(s.Length)]).ToArray());
}
private string HashCode(string code)
{
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(code));
return Convert.ToHexString(hash);
}
}MFA Implementation in ASP.NET Core
// User model with MFA state
public class ApplicationUser : IdentityUser
{
public bool TwoFactorEnabled { get; set; }
public string? TotpSecret { get; set; }
public DateTime? MfaLockedUntil { get; set; }
public int FailedMfaAttempts { get; set; }
}
// MFA Controller
[Authorize]
public class MfaController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly TotpService _totp;
private readonly ILogger<MfaController> _logger;
[HttpPost("mfa/enable")]
public async Task<IActionResult> EnableMfa()
{
var user = await _userManager.GetUserAsync(User);
var secretKey = _totp.GenerateSecretKey();
// Store secret (encrypted)
user.TotpSecret = secretKey;
await _userManager.UpdateAsync(user);
// Return QR code URI for user to scan
var uri = _totp.GenerateUri(secretKey, "MyApp", user.Email!);
var qrCodeData = GenerateQrCode(uri); // Base64 PNG
return Ok(new { TotpSecret = secretKey, QrCode = qrCodeData });
}
[HttpPost("mfa/verify")]
public async Task<IActionResult> VerifyMfa([FromBody] MfaVerifyRequest request)
{
var user = await _userManager.GetUserAsync(User);
if (!_totp.VerifyCode(user.TotpSecret!, request.Code))
{
user.FailedMfaAttempts++;
if (user.FailedMfaAttempts >= 5)
{
user.MfaLockedUntil = DateTime.UtcNow.AddHours(1);
}
await _userManager.UpdateAsync(user);
return BadRequest(new { error = "Invalid code" });
}
user.FailedMfaAttempts = 0;
user.MfaLockedUntil = null;
await _userManager.UpdateAsync(user);
return Ok(new { success = true });
}
[HttpPost("mfa/disable")]
[RequireMfa] // Custom authorization filter
public async Task<IActionResult> DisableMfa()
{
var user = await _userManager.GetUserAsync(User);
user.TwoFactorEnabled = false;
user.TotpSecret = null;
await _userManager.UpdateAsync(user);
return Ok();
}
}
// Custom authorization attribute
public class RequireMfaAttribute : Attribute, IAsyncAuthorizationFilter
{
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var user = await context.HttpContext.GetUserManager<ApplicationUser>();
var userId = context.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (user == null || user.TwoFactorEnabled)
{
context.Result = new ForbidResult();
return;
}
}
}Federated Identity
Entra ID (Azure AD) Integration
// Install: Microsoft.Identity.Web
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCache();
// API protection
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
// Configuration (appsettings.json)
/*
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "<tenant-id>",
"ClientId": "<client-id>",
"ClientSecret": "<client-secret>",
"Scopes": ["user.read", "api://app/read"]
}
}
*/SAML 2.0
// Install: Sustainsys.Saml2
builder.Services.AddSaml2(options =>
{
options.SPOptions.EntityId = new EntityId("https://app.example.com/saml");
options.SPOptions.ReturnUrl = new Uri("https://app.example.com/");
options.IdentityProviders.Add(new IdentityProvider(
new EntityId("https://idp.example.com/saml"),
options.SPOptions)
{
MetadataLocation = "https://idp.example.com/metadata.xml",
AllowUnsolicitedAuthnResponse = true,
Wallet = new MetadataWireSyncMetadataStore()
});
options.SPOptions.ServiceCertificates.Add(
new X509Certificate2("path/to/cert.pfx", "password"));
});
app.UseSaml2();API Key Management
Generation, Scoping, Rotation, Revocation
public class ApiKeyService
{
private const int KeyLength = 32;
private readonly ApplicationDbContext _db;
private readonly ILogger<ApiKeyService> _logger;
public async Task<ApiKeyDto> GenerateKeyAsync(
string ownerId,
IEnumerable<string> scopes,
DateTimeOffset? expiry)
{
var rawKey = GenerateRandomKey();
var hashedKey = HashKey(rawKey);
var apiKey = new ApiKey
{
Id = Guid.NewGuid(),
OwnerId = ownerId,
Name = $"key-{DateTime.UtcNow:yyyyMMdd-HHmmss}",
KeyHash = hashedKey,
Scopes = scopes.ToList(),
ExpiresAt = expiry,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
_db.ApiKeys.Add(apiKey);
await _db.SaveChangesAsync();
_logger.LogInformation("API key generated for user {OwnerId}", ownerId);
return new ApiKeyDto
{
Id = apiKey.Id,
Name = apiKey.Name,
Key = rawKey, // Only returned once!
Scopes = apiKey.Scopes,
ExpiresAt = apiKey.ExpiresAt
};
}
public async Task<bool> ValidateKeyAsync(string rawKey, IEnumerable<string> requiredScopes)
{
var hashedKey = HashKey(rawKey);
var apiKey = await _db.ApiKeys.FirstOrDefaultAsync(k => k.KeyHash == hashedKey);
if (apiKey == null || !apiKey.IsActive)
return false;
if (apiKey.ExpiresAt.HasValue && apiKey.ExpiresAt < DateTime.UtcNow)
return false;
// Scope check
var missingScopes = requiredScopes.Except(apiKey.Scopes).ToList();
if (missingScopes.Any())
return false;
// Update usage
apiKey.LastUsedAt = DateTime.UtcNow;
apiKey.UsageCount++;
await _db.SaveChangesAsync();
return true;
}
public async Task RevokeKeyAsync(Guid keyId, string reason)
{
var apiKey = await _db.ApiKeys.FindAsync(keyId);
if (apiKey != null)
{
apiKey.IsActive = false;
apiKey.RevokedAt = DateTime.UtcNow;
apiKey.RevocationReason = reason;
await _db.SaveChangesAsync();
}
}
public async Task RotateKeyAsync(Guid keyId, string newOwnerName)
{
var newRawKey = GenerateRandomKey();
var newHashedKey = HashKey(newRawKey);
var apiKey = await _db.ApiKeys.FindAsync(keyId);
if (apiKey != null)
{
apiKey.KeyHash = newHashedKey;
apiKey.CreatedAt = DateTime.UtcNow;
apiKey.LastUsedAt = null;
apiKey.UsageCount = 0;
await _db.SaveChangesAsync();
_logger.LogInformation("API key {KeyId} rotated for {Owner}", keyId, newOwnerName);
}
}
private string GenerateRandomKey()
{
var bytes = new byte[KeyLength];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(bytes);
return $"sk-{Convert.ToHexString(bytes).ToLower()}";
}
private string HashKey(string key)
{
using var sha256 = SHA256.Create();
return Convert.ToHexString(sha256.ComputeHash(Encoding.UTF8.GetBytes(key)));
}
}
// Middleware for API Key validation
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private readonly ApiKeyService _apiKeyService;
private readonly ILogger<ApiKeyMiddleware> _logger;
public ApiKeyMiddleware(RequestDelegate next, ApiKeyService apiKeyService, ILogger<ApiKeyMiddleware> logger)
{
_next = next;
_apiKeyService = apiKeyService;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.Request.Headers.TryGetValue("X-API-Key", out var apiKeyHeader))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new { error = "API key required" });
return;
}
var requiredScopes = ExtractScopesFromRoute(context);
var isValid = await _apiKeyService.ValidateKeyAsync(apiKeyHeader, requiredScopes);
if (!isValid)
{
_logger.LogWarning("Invalid API key used from {Ip}", context.Connection.RemoteIpAddress);
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new { error = "Invalid or expired API key" });
return;
}
await _next(context);
}
private IEnumerable<string> ExtractScopesFromRoute(HttpContext context)
{
return context.Request.Path.Value?.Split('/')
.Where(s => !string.IsNullOrEmpty(s) && s != "api")
.Select(s => $"{s}.read") ?? Array.Empty<string>();
}
}Service-to-Service Authentication
mTLS for Internal Services
// Service A calls Service B via mTLS
public class ServiceClient
{
private readonly HttpClient _client;
public ServiceClient(IHttpClientFactory factory, X509Certificate2 clientCert)
{
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(clientCert);
handler.ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; // In production, validate properly
_client = factory.CreateClient("mtls");
_client = new HttpClient(handler);
}
public async Task<T> CallServiceAsync<T>(string endpoint)
{
var response = await _client.GetAsync(endpoint);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<T>();
}
}JWT with Client Credentials
// Service-to-service JWT using client credentials flow
public class ServiceTokenProvider
{
private readonly HttpClient _httpClient;
private readonly TokenClient _tokenClient;
private string? _cachedToken;
private DateTime _tokenExpiry;
public ServiceTokenProvider(HttpClient httpClient, IConfiguration config)
{
_httpClient = httpClient;
_tokenClient = new TokenClient(
config["AuthServer:TokenUrl"],
config["ServiceClientId"],
config["ServiceClientSecret"]);
}
public async Task<string> GetAccessTokenAsync(string scope)
{
// Return cached token if valid
if (_cachedToken != null && _tokenExpiry > DateTime.UtcNow.AddMinutes(1))
return _cachedToken;
// Request new token
var response = await _tokenClient.RequestClientCredentialsAsync(scope);
if (response.IsError)
throw new InvalidOperationException($"Token request failed: {response.Error}");
_cachedToken = response.AccessToken;
_tokenExpiry = DateTime.UtcNow.AddSeconds(response.ExpiresIn - 30);
return _cachedToken;
}
}Session Security
Secure Cookie Configuration
builder.Services.ConfigureApplicationCookie(options =>
{
// Security flags
options.Cookie.HttpOnly = true; // No JS access
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // HTTPS only
options.Cookie.SameSite = SameSiteMode.Strict; // CSRF protection
// Session management
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.SlidingExpiration = true;
options.AccessDeniedPath = "/account/access-denied";
options.LoginPath = "/account/login";
// Session fixation prevention
options.Events.OnRedirectToLogin = context =>
{
context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
return context.Response.Redirect(context.RedirectUri);
};
});Session Fixation Prevention
public class SessionSecurityMiddleware
{
private readonly RequestDelegate _next;
public SessionSecurityMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
// Detect session fixation attempts
var originalSessionId = context.Request.Cookies[".AspNetCore.Identity.Application"];
await _next(context);
// After authentication, ensure new session ID
if (context.User.Identity?.IsAuthenticated == true &&
originalSessionId != context.Response.Cookies[".AspNetCore.Identity.Application"])
{
// Session was properly regenerated
}
}
}Best Practices Summary
| Сценарий | Рекомендация |
|---|---|
| MFA | TOTP + backup codes, forced for admin |
| Federated | Entra ID for enterprise, OIDC for public |
| API Keys | Hashed storage, scoped, auto-rotation |
| Service-to-Service | mTLS + JWT client credentials |
| Sessions | Secure cookies, short expiry, regeneration |
| Passwords | Argon2id, min 12 chars, complexity rules |
Практика
- [ ] Реализовать MFA с TOTP и backup codes
- [ ] Настроить federated authentication с Entra ID (Azure AD)
- [ ] Создать API key management system с automatic rotation
Ссылки
- [TOTP RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238)
- [Microsoft.Identity.Web](https://github.com/AzureAD/microsoft-identity-web)
- [Sustainsys.Saml2](https://github.com/Sustainsys/Saml2)
Security Incident Response
Введение
Security Incident Response — практики обнаружения, реагирования и восстановления после security incidents.
Ключевые компоненты: logging, anomaly detection, incident response plan, forensic data collection.
Security Event Logging
Что логировать
| Событие | Уровень | Данные |
|---|---|---|
| Login success | Information | UserId, Ip, Timestamp |
| Login failure | Warning | Ip, Username (not password) |
| MFA failure | Warning | UserId, Ip, FailedAttempts |
| Access denied | Warning | UserId, Resource, Action |
| Password change | Information | UserId, Timestamp |
| Role change | Critical | AdminUserId, TargetUserId, NewRole |
| API key generated | Information | OwnerId, Scopes |
| Config changed | Critical | AdminUserId, ConfigKey, OldValue, NewValue |
| Data export | Warning | UserId, Entity, RecordCount |
| Certificate renewed | Information | CertName, RenewedBy |
Structured Logging
// Configure structured logging
builder.Logging.ClearProviders();
builder.Logging.AddJsonConsole(options =>
{
options.JsonFormatterOptions.IncludeScopes = true;
});
// Security event logger
public class SecurityEventLogger
{
private readonly ILogger<SecurityEventLogger> _logger;
private readonly IHttpContextAccessor _httpContext;
public SecurityEventLogger(
ILogger<SecurityEventLogger> logger,
IHttpContextAccessor httpContext)
{
_logger = logger;
_httpContext = httpContext;
}
public void LogLoginFailure(string username, string reason)
{
var ip = GetIpAddress();
_logger.LogWarning(
"Login failed: user={Username} ip={Ip} reason={Reason}",
username, ip, reason);
}
public void LogAccessDenied(string userId, string resource, string action)
{
var ip = GetIpAddress();
_logger.LogWarning(
"Access denied: user={UserId} resource={Resource} action={Action} ip={Ip}",
userId, resource, action, ip);
}
public void LogPrivilegeEscalation(string adminUser, string targetUser, string newRole)
{
_logger.LogCritical(
"PRIVILEGE ESCALATION: admin={AdminUser} target={TargetUser} role={NewRole}",
adminUser, targetUser, newRole);
}
private string GetIpAddress()
{
var context = _httpContext.HttpContext;
if (context == null) return "unknown";
var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
// Handle forwarded headers (behind load balancer)
if (context.Request.Headers.TryGetValue("X-Forwarded-For", out var forwarded))
{
ip = forwarded.First();
}
return ip;
}
}Tamper-Evident Audit Trail
Hash-Chained Audit Logs
Каждый audit record содержит хеш предыдущего — цепочка хешей обнаруживает подмену.
public class TamperEvidentAuditLogger
{
private readonly ApplicationDbContext _db;
private readonly ILogger<TamperEvidentAuditLogger> _logger;
private readonly SemaphoreSlim _lock = new(1, 1);
public TamperEvidentAuditLogger(
ApplicationDbContext db,
ILogger<TamperEvidentAuditLogger> logger)
{
_db = db;
_logger = logger;
}
public async Task LogAsync(AuditEvent evt)
{
await _lock.WaitAsync();
try
{
// Get previous hash for chain
var previousLog = await _db.AuditLogs
.OrderByDescending(l => l.Id)
.FirstOrDefaultAsync();
var previousHash = previousLog?.CurrentHash ?? "GENESIS";
// Calculate current hash
var hashInput = $"{evt.Type}:{evt.UserId}:{evt.Resource}:{evt.Timestamp}:{previousHash}";
var currentHash = ComputeHash(hashInput);
var auditLog = new AuditLog
{
Id = evt.Id,
Type = evt.Type,
UserId = evt.UserId,
Resource = evt.Resource,
Details = evt.Details,
IpAddress = evt.IpAddress,
PreviousHash = previousHash,
CurrentHash = currentHash,
Timestamp = DateTime.UtcNow
};
_db.AuditLogs.Add(auditLog);
await _db.SaveChangesAsync();
// Periodic anchoring (every 100 records)
var count = await _db.AuditLogs.CountAsync();
if (count % 100 == 0)
{
await AnchorHashAsync(currentHash);
}
}
finally
{
_lock.Release();
}
}
private string ComputeHash(string input)
{
using var sha256 = SHA256.Create();
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes);
}
private async Task AnchorHashAsync(string hash)
{
// Store hash in external secure location (email, immutable storage)
// This prevents DBA from rewriting history
var anchor = new HashAnchor
{
AnchorHash = hash,
AnchorAtId = await _db.AuditLogs.MaxAsync(l => l.Id),
AnchoredAt = DateTime.UtcNow
};
_db.HashAnchors.Add(anchor);
await _db.SaveChangesAsync();
}
}
// Verification engine
public class AuditTrailVerifier
{
private readonly ApplicationDbContext _db;
public AuditTrailVerifier(ApplicationDbContext db) => _db = db;
public async Task<VerificationResult> VerifyAsync()
{
var logs = await _db.AuditLogs.OrderBy(l => l.Id).ToListAsync();
var anchors = await _db.HashAnchors.ToListAsync();
string expectedPreviousHash = "GENESIS";
var violations = new List<Violation>();
foreach (var log in logs)
{
if (log.PreviousHash != expectedPreviousHash)
{
violations.Add(new Violation
{
LogId = log.Id,
Type = "HashChainBroken",
Message = $"Expected {expectedPreviousHash}, got {log.PreviousHash}"
});
}
var computedHash = ComputeHash(
$"{log.Type}:{log.UserId}:{log.Resource}:{log.Timestamp}:{log.PreviousHash}");
if (computedHash != log.CurrentHash)
{
violations.Add(new Violation
{
LogId = log.Id,
Type = "PayloadTampered",
Message = $"Hash mismatch for log {log.Id}"
});
}
expectedPreviousHash = log.CurrentHash;
}
return new VerificationResult
{
IsValid = violations.Count == 0,
Violations = violations,
TotalRecords = logs.Count,
AnchorsVerified = anchors.Count
};
}
private string ComputeHash(string input)
{
using var sha256 = SHA256.Create();
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes);
}
}Brute Force Detection
Automatic Account Lockout
public class BruteForceDetector
{
private readonly IDatabaseCache _cache;
private readonly ILogger<BruteForceDetector> _logger;
private const int MaxAttempts = 5;
private const int LockoutMinutes = 15;
private const int ProgressiveDelaySeconds = 10;
public async Task<BruteForceResult> CheckAsync(string identifier)
{
var key = $"bf:{identifier}";
var attempts = await _cache.GetIntAsync(key) ?? 0;
if (attempts >= MaxAttempts)
{
var lockedUntil = await _cache.GetStringAsync($"{key}:locked");
if (DateTimeOffset.TryParse(lockedUntil, out var lockTime) &&
lockTime > DateTimeOffset.UtcNow)
{
_logger.LogWarning("Account locked: {Identifier} attempts={Attempts}",
identifier, attempts);
return BruteForceResult.Locked((int)(lockTime - DateTimeOffset.UtcNow).TotalMinutes);
}
else
{
// Lock expired, reset
await _cache.RemoveAsync(key);
await _cache.RemoveAsync($"{key}:locked");
}
}
return BruteForceResult.Continue(attempts);
}
public async Task RecordFailureAsync(string identifier)
{
var key = $"bf:{identifier}";
var attempts = await _cache.GetIntAsync(key) ?? 0;
attempts++;
await _cache.SetIntAsync(key, attempts);
await _cache.SetExpiryAsync(key, TimeSpan.FromMinutes(30));
if (attempts >= MaxAttempts)
{
var lockUntil = DateTimeOffset.UtcNow.AddMinutes(LockoutMinutes);
await _cache.SetStringAsync($"{key}:locked", lockUntil.ToString());
await _cache.SetExpiryAsync($"{key}:locked",
TimeSpan.FromMinutes(LockoutMinutes + 1));
_logger.LogWarning("Account locked after {Attempts} failed attempts: {Identifier}",
attempts, identifier);
}
}
public async Task ResetAsync(string identifier)
{
await _cache.RemoveAsync($"bf:{identifier}");
await _cache.RemoveAsync($"bf:{identifier}:locked");
}
}
// Login endpoint with brute force protection
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var detector = new BruteForceDetector(_cache, _logger);
// Check if account is locked
var result = await detector.CheckAsync(request.Username);
if (result.IsLocked)
{
return TooManyRequests($"Account locked for {result.LockoutMinutes} minutes");
}
// Progressive delay
if (result.Attempts > 0)
{
await Task.Delay(TimeSpan.FromSeconds(result.Attempts * ProgressiveDelaySeconds));
}
// Attempt login
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null || !await _userManager.CheckPasswordAsync(user, request.Password))
{
await detector.RecordFailureAsync(request.Username);
return Unauthorized();
}
// Success - reset counter
await detector.ResetAsync(request.Username);
// Login logic...
return Ok();
}Anomaly Detection
Unusual Access Patterns
public class AnomalyDetector
{
private readonly ApplicationDbContext _db;
private readonly ILogger<AnomalyDetector> _logger;
public async Task DetectAsync(SecurityEvent evt)
{
// 1. Geographic anomaly
if (await IsGeographicAnomaly(evt.UserId, evt.IpAddress))
{
_logger.LogWarning("GEOGRAPHIC ANOMALY: user={UserId} ip={Ip}",
evt.UserId, evt.IpAddress);
await AlertSecurityTeamAsync(evt);
}
// 2. Time anomaly
if (await IsTimeAnomaly(evt.UserId, DateTime.UtcNow))
{
_logger.LogWarning("TIME ANOMALY: user={UserId} hour={Hour}",
evt.UserId, DateTime.UtcNow.Hour);
await AlertSecurityTeamAsync(evt);
}
// 3. Volume anomaly
if (await IsVolumeAnomaly(evt.UserId, evt.Type))
{
_logger.LogWarning("VOLUME ANOMALY: user={UserId} type={Type}",
evt.UserId, evt.Type);
await AlertSecurityTeamAsync(evt);
}
// 4. Privilege anomaly
if (evt.Type == "RoleChange" || evt.Type == "PermissionGrant")
{
_logger.LogCritical("PRIVILEGE ANOMALY: {Event}", evt.Details);
await AlertSecurityTeamAsync(evt);
}
}
private async Task<bool> IsGeographicAnomaly(string userId, string ipAddress)
{
var recentEvents = await _db.SecurityEvents
.Where(e => e.UserId == userId &&
e.Timestamp > DateTime.UtcNow.AddHours(-1))
.ToListAsync();
if (recentEvents.Count < 2) return false;
var ips = recentEvents.Select(e => e.IpAddress).Distinct().ToList();
return ips.Count > 3; // More than 3 different IPs in 1 hour
}
private async Task<bool> IsTimeAnomaly(string userId, DateTime now)
{
var businessHours = now.Hour is >= 7 and <= 19 &&
now.DayOfWeek is not (DayOfWeek.Saturday or DayOfWeek.Sunday);
if (businessHours) return false;
var afterHoursCount = await _db.SecurityEvents
.CountAsync(e => e.UserId == userId &&
e.Timestamp > DateTime.UtcNow.AddHours(-1) &&
!businessHours);
return afterHoursCount > 5;
}
private async Task<bool> IsVolumeAnomaly(string userId, string eventType)
{
var count = await _db.SecurityEvents
.CountAsync(e => e.UserId == userId &&
e.Type == eventType &&
e.Timestamp > DateTime.UtcNow.AddMinutes(-5));
return count > 100; // More than 100 events in 5 minutes
}
private async Task AlertSecurityTeamAsync(SecurityEvent evt)
{
// Send to SIEM, Slack, Email
_logger.LogCritical("SECURITY ALERT: {Event}", evt.Details);
}
}Incident Response Plan
Phases
1. Preparation
- Security tools (SIEM, EDR, WAF)
- Team roles and contacts
- Communication plan
2. Identification
- Detect anomaly (automated or manual)
- Classify severity (P1-P4)
- Create incident ticket
3. Containment
- Short-term: isolate affected system
- Long-term: patch, rotate credentials
4. Eradication
- Remove attacker access
- Clean compromised systems
- Patch vulnerability
5. Recovery
- Restore from clean backup
- Monitor for recurrence
- Validate system integrity
6. Lessons Learned
- Post-incident review
- Update playbooks
- Improve detectionSeverity Classification
| Severity | Description | Response Time | Example |
|---|---|---|---|
| P1 (Critical) | Active data breach, system compromise | 15 min | Ransomware, SQL injection with data exfil |
| P2 (High) | Potential breach, critical vulnerability | 1 hour | Credential leak, privilege escalation |
| P3 (Medium) | Suspicious activity, policy violation | 4 hours | Brute force attack, unusual access pattern |
| P4 (Low) | Informational, minor policy issue | 24 hours | Failed login spike, config drift |
Playbook: SQL Injection Attack
1. Detect: WAF/SIEM alerts on SQL injection pattern
2. Contain:
- Block attacking IP via WAF
- Disable affected API endpoint
- Preserve logs and evidence
3. Assess:
- Determine if data was exfiltrated
- Check database for unauthorized queries
- Review audit trail
4. Eradicate:
- Fix vulnerable query (parameterize)
- Rotate database credentials
- Review all endpoints for similar issues
5. Recover:
- Re-enable endpoint after fix
- Monitor for recurrence
6. Report:
- Document timeline and impact
- Notify affected parties if needed
- Update security controlsForensic Data Collection
What to Preserve
| Data Type | Retention | Format |
|---|---|---|
| Authentication logs | 1 year | Structured JSON |
| Authorization events | 7 years | Immutable storage |
| Network flow data | 90 days | PCAP / NetFlow |
| File integrity logs | 1 year | Hash-based |
| Configuration changes | 7 years | Version-controlled |
| User activity logs | 1 year | Append-only |
Evidence Preservation
public class EvidencePreservationService
{
private readonly ICloudStorage _storage;
private readonly ILogger<EvidencePreservationService> _logger;
public async Task PreserveEvidenceAsync(Incident incident)
{
var evidenceId = Guid.NewGuid();
var container = $"evidence/{incident.Id}/{evidenceId}";
// 1. Memory dump (if applicable)
// 2. Disk image (if applicable)
// 3. Log collection
var logs = await CollectLogsAsync(incident);
await _storage.UploadAsync(container, "logs.json", logs);
// 4. Configuration snapshot
var config = await CollectConfigurationAsync();
await _storage.UploadAsync(container, "config.json", config);
// 5. Hash all evidence
var manifest = new EvidenceManifest
{
EvidenceId = evidenceId,
IncidentId = incident.Id,
CollectedAt = DateTime.UtcNow,
Files = new List<EvidenceFile>
{
new() { Name = "logs.json", Hash = ComputeHash(logs) },
new() { Name = "config.json", Hash = ComputeHash(config) }
}
};
await _storage.UploadAsync(container, "manifest.json",
JsonSerializer.Serialize(manifest));
// Store manifest hash in immutable storage
await _storage.UploadAsync("evidence-manifests",
evidenceId.ToString(), ComputeHash(JsonSerializer.Serialize(manifest)));
_logger.LogInformation("Evidence preserved: {EvidenceId} for incident {IncidentId}",
evidenceId, incident.Id);
}
}Best Practices
- Automate logging — manual logging is error-prone
- Immutable audit trails — tamper-evident design
- Centralize logs — SIEM for correlation
- Regular testing — run incident response drills
- Document playbooks — reduce response time
- Retain evidence — meet compliance requirements
- Monitor 24/7 — automated alerting
- Train team — regular security awareness
Практика
- [ ] Реализовать security event logging с tamper-evident audit trail
- [ ] Создать brute force detection и automatic account lockout
- [ ] Написать incident response playbook для common attack scenarios
Ссылки
- [NIST Incident Handling Guide](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-61r2.pdf)
- [OWASP Logging Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html)
- [EF Core Interceptors for Audit](https://learn.microsoft.com/en-us/ef/core/miscellaneous/interceptors)
Контрольная точка модуля 7
Контрольная точка модуля 7
Описание
Создать полноценную Secure API Platform, объединяющую все концепции безопасности из модуля.
Требования
Архитектура
┌─────────────────────────────────────────────────────┐
│ SPA / Mobile │
└────────────────────┬────────────────────────────────┘
│ HTTPS + JWT / OAuth2
▼
┌─────────────────────────────────────────────────────┐
│ API Gateway / Reverse Proxy │
│ (Rate Limiting, CORS, Security Headers) │
└────────────────────┬────────────────────────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌────────────┐ ┌──────────┐ ┌──────────┐
│ Auth Svc │ │ API Svc │ │ User Svc │
│ (OIDC/ │ │ (RBAC + │ │ (ABAC + │
│ MFA) │ │ HMAC) │ │ Audit) │
└─────┬──────┘ └────┬─────┘ └────┬─────┘
│ │ │
└──────────────┼────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ Shared Infrastructure │
│ Redis (Rate Limiting, Sessions) │
│ SQL Server (Data, Audit Logs) │
│ Azure Key Vault (Secrets, Certificates) │
│ SIEM / Log Analytics (Monitoring) │
└─────────────────────────────────────────────────────┘Компоненты
OAuth 2.0 + OIDC Authentication с PKCE Flow
// Auth Service — OIDC provider
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://auth.example.com";
options.ClientId = "secure-api-client";
options.ResponseType = OpenIdConnectResponseType.Code;
options.UsePkce = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("api://secure-api.read");
options.Scope.Add("api://secure-api.write");
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
});
// JWT token issuance
public class TokenService
{
public (string AccessToken, RefreshToken RefreshToken) GenerateTokens(
ApplicationUser user, IEnumerable<string> roles)
{
// RS256 signing
using var rsa = RSA.Create(2048);
rsa.ImportFromPem(File.ReadAllText("private-key.pem"));
var securityKey = new RsaSecurityKey(rsa);
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256);
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.UniqueName, user.Email!),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
};
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
var token = new JwtSecurityToken(
issuer: "https://auth.example.com",
audience: "https://secure-api.example.com",
claims: claims,
expires: DateTime.UtcNow.AddMinutes(15),
signingCredentials: credentials);
var accessToken = new JwtSecurityTokenHandler().WriteToken(token);
var refreshToken = new RefreshToken
{
Token = HashToken(Guid.NewGuid().ToString()),
UserId = user.Id,
Expires = DateTime.UtcNow.AddDays(14),
Created = DateTime.UtcNow
};
return (accessToken, refreshToken);
}
}RBAC + ABAC Hybrid Authorization
// RBAC — Role-based
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
options.AddPolicy("ManagerOrAbove", policy =>
policy.Requirements.Add(new RoleHierarchyRequirement("Manager")));
// ABAC — Attribute-based
options.AddPolicy("DepartmentAccess", policy =>
policy.Requirements.Add(new DepartmentMatchRequirement()));
// Hybrid — RBAC + ABAC
options.AddPolicy("SensitiveDataAccess", policy =>
{
policy.RequireRole("Admin", "Manager");
policy.Requirements.Add(new SecurityLevelRequirement(3));
policy.Requirements.Add(new DepartmentMatchRequirement());
});
});
// ABAC Handler
public class DepartmentMatchRequirement : IAuthorizationRequirement { }
public class DepartmentMatchHandler : AuthorizationHandler<DepartmentMatchRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DepartmentMatchRequirement requirement)
{
var userDept = context.User.FindFirstValue("department");
var resourceDept = GetResourceDepartment(context.Resource);
if (userDept == resourceDept)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
// Security Level Requirement
public class SecurityLevelRequirement : IAuthorizationRequirement
{
public int RequiredLevel { get; }
public SecurityLevelRequirement(int level) => RequiredLevel = level;
}
public class SecurityLevelHandler : AuthorizationHandler<SecurityLevelRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
SecurityLevelRequirement requirement)
{
var userLevel = int.Parse(context.User.FindFirstValue("security_level") ?? "0");
if (userLevel >= requirement.RequiredLevel)
context.Succeed(requirement);
return Task.CompletedTask;
}
}Rate Limiting, CORS, CSRF Protection
// Distributed rate limiting with Redis
builder.Services.AddSingleton<IConnectionMultiplexer>(
ConnectionMultiplexer.Connect("redis://cache.example.com:6379"));
builder.Services.AddSingleton<RedisRateLimiter>();
builder.Services.AddMiddleware<RateLimitMiddleware>();
// CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("SecureApi", policy =>
policy.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders(HeaderNames.Authorization, HeaderNames.ContentType)
.AllowCredentials());
});
// CSRF
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
});Encrypted Secrets через Key Vault
// Azure Key Vault integration
var credential = new DefaultAzureCredential();
var vaultUri = new Uri("https://myvault.vault.azure.net/");
builder.Configuration.AddAzureKeyVault(vaultUri, credential);
// Data Protection with Key Vault
services.AddDataProtection()
.SetApplicationName("SecureApi")
.ProtectKeysWithAzureKeyVault(
new Uri("https://myvault.vault.azure.net/keys/dpkey"),
credential);
// Secret access with auditing
public class AuditedSecretService
{
private readonly SecretClient _client;
private readonly SecurityEventLogger _logger;
public async Task<string> GetSecretAsync(string name)
{
var secret = await _client.GetSecretAsync(name);
_logger.LogSecretAccess(name, "secret-service", "internal");
return secret.Value;
}
}Comprehensive Security Logging и Audit Trail
// Tamper-evident audit logging
public class SecurityAuditService
{
private readonly ApplicationDbContext _db;
private readonly SecurityEventLogger _eventLogger;
private readonly AnomalyDetector _anomalyDetector;
public async Task LogSecurityEvent(SecurityEvent evt)
{
// 1. Log to audit trail (hash-chained)
await _auditLogger.LogAsync(evt);
// 2. Log for SIEM
_eventLogger.Log(evt);
// 3. Check for anomalies
await _anomalyDetector.DetectAsync(evt);
// 4. Check brute force
if (evt.Type == "LoginFailure")
{
await _bruteForceDetector.RecordFailureAsync(evt.UserId);
}
}
}
// EF Core Interceptor for automatic audit
public class AuditInterceptor : SaveChangesInterceptor
{
public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
if (eventData.Context == null) return await base.SavingChangesAsync(eventData, result, cancellationToken);
var audits = new List<AuditLog>();
foreach (var entry in eventData.Context.ChangeTracker.Entries())
{
if (entry.Entity is IAuditableEntity auditable)
{
audits.Add(new AuditLog
{
EntityType = entry.Entity.GetType().Name,
EntityId = GetPrimaryKey(entry),
Action = entry.State.ToString(),
UserId = auditable.ModifiedBy,
OldValues = entry.OriginalValues.ToDictionary(),
NewValues = entry.CurrentValues.ToDictionary(),
Timestamp = DateTime.UtcNow
});
}
}
// Save audit logs
if (audits.Any())
{
eventData.Context.AuditLogs.AddRange(audits);
await eventData.Context.SaveChangesAsync(cancellationToken);
}
return await base.SavingChangesAsync(eventData, result, cancellationToken);
}
}Automated Vulnerability Scanning в CI Pipeline
# .github/workflows/security.yml
name: Security Pipeline
on: [push, pull_request, workflow_dispatch]
jobs:
sast:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
- name: Run CodeQL
uses: github/codeql-action/init@v3
with: { languages: csharp }
- name: Analyze
uses: github/codeql-action/analyze@v3
dependency-audit:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
- name: Audit
run: dotnet list package --vulnerable --include-transitive
- name: Run Snyk
uses: snyk/actions/dotnet@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
container-scan:
name: Container Scan
runs-on: ubuntu-latest
steps:
- name: Build image
run: docker build -t secure-api:${{ github.sha }} .
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: secure-api:${{ github.sha }}
severity: HIGH,CRITICAL
dast:
name: Dynamic Analysis
runs-on: ubuntu-latest
needs: [sast, dependency-audit]
steps:
- name: Start app
run: docker compose up -d
- name: Run OWASP ZAP
uses: zaproxy/action-full-scan@v0.8.0
with:
target: 'http://localhost:5000'
security-gate:
name: Security Gate
needs: [sast, dependency-audit, container-scan, dast]
runs-on: ubuntu-latest
if: always()
steps:
- name: Check results
run: |
if [ "${{ needs.sast.result }}" != "success" ] || \
[ "${{ needs.dependency-audit.result }}" != "success" ] || \
[ "${{ needs.container-scan.result }}" != "success" ] || \
[ "${{ needs.dast.result }}" != "success" ]; then
echo "Security gate FAILED"
exit 1
fi
echo "Security gate PASSED"Критерии прохождения
Security Requirements
- [ ] Zero OWASP Top 10 vulnerabilities при automated scan
- CodeQL / SAST passes with no findings
- OWASP ZAP / DAST passes with no HIGH/CRITICAL
dotnet list package --vulnerablereturns no results
- [ ] All sensitive data encrypted at rest и in transit
- TLS 1.3 enforced
- Passwords hashed with Argon2id / PBKDF2
- PII encrypted with AES-GCM
- Connection strings in Key Vault
- [ ] Security event audit trail с tamper-evident design
- Hash-chained audit logs
- External anchoring of hashes
- Verification engine works correctly
- [ ] Successful penetration test на common attack vectors
- SQL injection — all queries parameterized
- XSS — output encoding verified
- CSRF — anti-forgery tokens working
- IDOR — ownership checks in place
- Brute force — account lockout active
- [ ] MFA enabled для admin operations
- TOTP-based MFA for admin accounts
- Backup codes generated and stored securely
- MFA required for privilege escalation
Functional Requirements
- [ ] OAuth 2.0 + OIDC с PKCE flow working
- [ ] JWT RS256 signing + token rotation
- [ ] RBAC + ABAC hybrid authorization
- [ ] Distributed rate limiting with Redis
- [ ] Secure CORS configuration
- [ ] Azure Key Vault integration with Managed Identity
- [ ] Automated CI/CD security pipeline
- [ ] Brute force protection
- [ ] Anomaly detection alerts
Структура проекта
SecureApiPlatform/
├── src/
│ ├── SecureApi.Auth/ # Auth service (OIDC, JWT, MFA)
│ │ ├── Controllers/
│ │ ├── Services/
│ │ │ ├── TokenService.cs
│ │ │ ├── MfaService.cs
│ │ │ └── RefreshTokenService.cs
│ │ └── Program.cs
│ │
│ ├── SecureApi.Api/ # Main API
│ │ ├── Controllers/
│ │ ├── Middleware/
│ │ │ ├── RateLimitMiddleware.cs
│ │ │ ├── SecurityHeadersMiddleware.cs
│ │ │ └── AuditMiddleware.cs
│ │ ├── Authorization/
│ │ │ ├── Handlers/
│ │ │ ├── Requirements/
│ │ │ └── Policies.cs
│ │ └── Program.cs
│ │
│ ├── SecureApi.Shared/ # Shared
│ │ ├── Models/
│ │ ├── Interfaces/
│ │ ├── Extensions/
│ │ └── SecurityEvents.cs
│ │
│ └── SecureApi.Infrastructure/ # Infrastructure
│ ├── Data/
│ │ ├── ApplicationDbContext.cs
│ │ └── Interceptors/
│ │ └── AuditInterceptor.cs
│ ├── Security/
│ │ ├── Cryptography/
│ │ ├── KeyVault/
│ │ └── RateLimiting/
│ └── Monitoring/
│ ├── Logging/
│ └── AnomalyDetection/
│
├── tests/
│ ├── SecureApi.UnitTests/
│ ├── SecureApi.IntegrationTests/
│ └── SecureApi.SecurityTests/
│
├── docker-compose.yml
├── .github/workflows/security.yml
└── README.mdОценка
| Критерий | Вес | Оценка |
|---|---|---|
| OAuth 2.0 + OIDC с PKCE | 15% | |
| JWT RS256 + token rotation | 10% | |
| RBAC + ABAC authorization | 15% | |
| Rate limiting + CORS + CSRF | 10% | |
| Key Vault + secrets management | 10% | |
| Tamper-evident audit trail | 10% | |
| CI/CD security pipeline | 10% | |
| MFA + brute force protection | 10% | |
| Документация и код-ревью | 10% | |
| Итого | 100% |
Ссылки
- [OAuth 2.0 RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749)
- [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html)
- [OWASP Testing Guide](https://owasp.org/www-project-web-security-testing-guide/)
- [MITRE ATT&CK](https://attack.mitre.org/)