07Безопасность (Security)

Уровень 1: Foundation

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() .AddEntityFrameworkStores();

// 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 InjectionLDAP)(uid=)(
XXEXML parser
NoSQL InjectionMongoDB{"\: ""}

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 => { options.Password.RequiredLength = 12; options.Password.RequiredUniqueChars = 4; options.Password.RequireNonAlphanumeric = true; options.Password.RequireLowercase = true; options.Password.RequireUppercase = true; options.Password.RequireDigit = true;

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 FetchUrl(string url) { if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) throw new ArgumentException("Invalid URL");

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

ЧастьСодержание
Headeralg (algorithm), typ (token type), kid (key ID)
Payloadiss, aud, exp, nbf, sub, jti, custom claims
SignatureHMAC/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 opt, string privateKeyPath) { _options = opt.Value; using var rsa = RSA.Create(); rsa.ImportFromPem(File.ReadAllText(privateKeyPath)); var rsaKey = new RsaSecurityKey(rsa) { KeyId = Guid.NewGuid().ToString("N") }; _credentials = new SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha256); }

public (string token, DateTimeOffset expires) CreateAccessToken( string userId, string userName, IEnumerable roles) { var claims = new List { new(JwtRegisteredClaimNames.Sub, userId), new(JwtRegisteredClaimNames.UniqueName, userName), 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: _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 CodeWeb apps (backend)Высокая
Authorization Code + PKCESPAs, mobile appsВысокая
Client CredentialsService-to-serviceСредняя
ImplicitУстарелНизкая
Device CodeIoT, 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

ХарактеристикаCookieJWT Bearer
StateServer-side sessionStateless
RevocationEasy (delete session)Hard (need token blacklist)
ScalabilityNeed shared session storeHorizontal scale
Cross-domainNativeCORS required
SPABFF pattern recommendedDirect bearer
MobileNot idealStandard
CSRFNeed anti-forgeryNot vulnerable

ASP.NET Core Identity

Базовая настройка

\\\csharp services.AddIdentity(options => { // Password settings options.Password.RequiredLength = 12; options.Password.RequiredUniqueChars = 4; options.Password.RequireNonAlphanumeric = true;

// 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() .AddDefaultTokenProviders() .AddPasswordValidator(); \\\

Custom Password Validator

\\\csharp public class CustomPasswordValidator : IPasswordValidator { public async Task ValidateAsync( UserManager manager, ApplicationUser user, string password) { var errors = new List();

// 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-GCMGeneral purpose — основной выбор
ChaCha20Mobile/IoT, платформы без AES-NI
AES-CBCLegacy 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

ХарактеристикаRSAECC (P-256)
Ключ 128-bit security3072 bits256 bits
Размер подписи256 bytes (RSA-2048)64 bytes (P-256)
Производительность (подпись)МедленнееБыстрее
Производительность (верификация)МедленнееБыстрее
ПрименениеKey exchange, legacyModern 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\$\$ return Convert.ToBase64String(salt) + ":" + Convert.ToBase64String(hash); }

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 SignedRequest(HttpClient client, string url, byte[] signingKey) { var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var body = "{\"data\": \"test\"}"; var payload = $"{timestamp}\\n{body}";

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

  1. Никогда не используйте MD5, SHA-1, DES, RC4 для нового кода
  2. AES-GCM — основной выбор для симметричного шифрования
  3. ECC предпочтительнее RSA для нового кода
  4. Argon2id или bcrypt для password hashing
  5. Constant-time comparison для sensitive data (CryptographicOperations.FixedTimeEquals)
  6. Unique nonce для каждой операции шифрования
  7. Key rotation — регулярно обновляйте ключи
  8. 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> _hierarchy = new() { { "SuperAdmin", new() { "SuperAdmin", "Admin", "Manager", "Viewer" } }, { "Admin", new() { "Admin", "Manager", "Viewer" } }, { "Manager", new() { "Manager", "Viewer" } }, { "Viewer", new() { "Viewer" } } };

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 { private readonly RoleHierarchyService _hierarchy;

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 EvaluateAsync( ClaimsPrincipal user, Resource resource, Action action, HttpContext context) { // User attributes var department = user.FindFirstValue("department"); var level = int.Parse(user.FindFirstValue("security_level") ?? "0"); var team = user.FindFirstValue("team");

// 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(); builder.Services.AddAuthorization(options => { options.AddPolicy("AbacAccess", policy => policy.Requirements.Add(new AbacRequirement())); });

public class AbacRequirement : IAuthorizationRequirement { }

public class AbacHandler : AuthorizationHandler { private readonly AbacPolicyEngine _engine;

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 AuthorizeAsync( ClaimsPrincipal user, string resource, string action) { var input = new { user = new { id = user.FindFirstValue(ClaimTypes.NameIdentifier), roles = user.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList(), department = user.FindFirstValue("department"), level = int.Parse(user.FindFirstValue("security_level") ?? "0") }, resource, action };

var response = await _opaClient.PostAsJsonAsync( $"{_policyUrl}/v1/data/netapp/authorization", input);

var result = await response.Content.ReadFromJsonAsync(); return result?.Allow == true; } }

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 { private readonly IUserService _userService;

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 UpdateDocument(int id, UpdateDocumentRequest request) { var doc = await _context.Documents.FindAsync(id); if (doc == null) return NotFound();

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 { private readonly IServiceProvider _services;

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 options) => _fallback = new DefaultAuthorizationPolicyProvider(options.Value);

public string PolicyName => _fallback.PolicyName;

public Task GetPolicyAsync(string policyName) { // Dynamic: any "Permission.XXX" policy is resolved on-demand if (policyName.StartsWith("Permission.")) { var permission = policyName.Substring("Permission.".Length); return Task.FromResult( new AuthorizationPolicyBuilder() .AddRequirements(new PermissionRequirement(permission)) .Build()); } return _fallback.GetFallbackPolicyAsync(); }

// ... other interface members }

// Registration builder.Services.AddSingleton(); builder.Services.AddScoped(); \\\


Сравнение паттернов

ПаттернСложностьГибкостьСценарий
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 BucketBurst + average rateAPI с 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 CheckAsync(string key, int limit, TimeSpan window) { var redisKey = $"rl:{key}"; var result = await _redis.EvaluateAsync( _luaScript, new RedisKey[] { redisKey }, new RedisValue[] { limit, (int)window.TotalSeconds });

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(options => { options.MinimumSameSitePolicy = SameSiteMode.Strict; // Or Lax for forms, Strict for APIs });

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 { public CreateUserValidator() { RuleFor(x => x.Email) .NotEmpty().WithMessage("Email is required") .EmailAddress().WithMessage("Invalid email format") .MaximumLength(255).WithMessage("Email too long");

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 _validator;

public UsersController(IValidator validator) => _validator = validator;

[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 Items { get; set; } }

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 configuration

User 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/myvault

Certificate 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 DPAPI

Zero-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 Account

Best Practices

  1. Никогда не хранить secrets в коде или git
  2. Use Managed Identity - без client secrets
  3. Rotate secrets regularly (90 days recommended)
  4. Least privilege - minimum permissions для каждого сервиса
  5. Encrypt secrets at rest и in transit
  6. Audit all secret access
  7. Use Key Vault references в App Service - no code changes needed
  8. 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

  1. Never trust, always verify — ни один запрос не доверяется автоматически
  2. Least privilege access — минимальные права для каждого запроса
  3. 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)

ThreatQuestionExample
SpoofingCan someone impersonate a user/service?Stolen JWT token
TamperingCan data be modified in transit?Man-in-the-middle
RepudiationCan someone deny an action?No audit log
Information DisclosureCan sensitive data be leaked?Error message with stack trace
DoSCan the service be made unavailable?Unbounded request loop
Elevation of PrivilegeCan 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 assumptions

Example: 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

RequirementImplementation
Data minimizationCollect only necessary data
Right to be forgottenDelete user data on request
Data portabilityExport user data in standard format
Consent managementTrack and manage user consent
Breach notificationAlert within 72 hours

SOC 2

Trust Service CriterionTechnical Controls
SecurityAuth, encryption, monitoring
AvailabilitySLA monitoring, backups, DR
Processing IntegrityData validation, error handling
ConfidentialityNDA, access controls, encryption
PrivacyData handling, consent management

ISO 27001

Control AreaTechnical Implementation
A.9 Access ControlRBAC, MFA, session management
A.10 CryptographyTLS, encryption at rest, key management
A.12 OperationsLogging, monitoring, backup
A.14 System SecuritySecure SDLC, vulnerability management
A.16 Incident MgmtSIEM, 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

СценарийРекомендация
MFATOTP + backup codes, forced for admin
FederatedEntra ID for enterprise, OIDC for public
API KeysHashed storage, scoped, auto-rotation
Service-to-ServicemTLS + JWT client credentials
SessionsSecure cookies, short expiry, regeneration
PasswordsArgon2id, 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 successInformationUserId, Ip, Timestamp
Login failureWarningIp, Username (not password)
MFA failureWarningUserId, Ip, FailedAttempts
Access deniedWarningUserId, Resource, Action
Password changeInformationUserId, Timestamp
Role changeCriticalAdminUserId, TargetUserId, NewRole
API key generatedInformationOwnerId, Scopes
Config changedCriticalAdminUserId, ConfigKey, OldValue, NewValue
Data exportWarningUserId, Entity, RecordCount
Certificate renewedInformationCertName, 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 detection

Severity Classification

SeverityDescriptionResponse TimeExample
P1 (Critical)Active data breach, system compromise15 minRansomware, SQL injection with data exfil
P2 (High)Potential breach, critical vulnerability1 hourCredential leak, privilege escalation
P3 (Medium)Suspicious activity, policy violation4 hoursBrute force attack, unusual access pattern
P4 (Low)Informational, minor policy issue24 hoursFailed 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 controls

Forensic Data Collection

What to Preserve

Data TypeRetentionFormat
Authentication logs1 yearStructured JSON
Authorization events7 yearsImmutable storage
Network flow data90 daysPCAP / NetFlow
File integrity logs1 yearHash-based
Configuration changes7 yearsVersion-controlled
User activity logs1 yearAppend-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

  1. Automate logging — manual logging is error-prone
  2. Immutable audit trails — tamper-evident design
  3. Centralize logs — SIEM for correlation
  4. Regular testing — run incident response drills
  5. Document playbooks — reduce response time
  6. Retain evidence — meet compliance requirements
  7. Monitor 24/7 — automated alerting
  8. 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 --vulnerable returns 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 с PKCE15%
JWT RS256 + token rotation10%
RBAC + ABAC authorization15%
Rate limiting + CORS + CSRF10%
Key Vault + secrets management10%
Tamper-evident audit trail10%
CI/CD security pipeline10%
MFA + brute force protection10%
Документация и код-ревью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/)