Client security fixes (cmd/tailscale-tray/main.go): - SSRF protection in Add Server dialog (validateControlURL): reject private/loopback/link-local/cloud-metadata IPs via DNS resolution - RCE gate on AuthURL/BrowseToURL exec paths (validateAuthURL) - Sanitized URL logging (sanitizeURLForLog drops query auth tokens) - Error handling on exec.Command with user-facing showError() Admin panel security (web-admin): - Bcrypt password hashing (replaces SHA256) - Rate limiting: 5 failed logins → 15-min lockout - Session + login attempt cleanup goroutine (hourly) - url.QueryEscape / encodeURIComponent for all API params - Fail-hard startup when no TLS and non-loopback bind - ADMIN_PASSWORD required (no default), password min 12 chars - Username regex whitelist Installer hardening (Setup.wxs): - util:PermissionEx restricts SCM access: only Administrators + SYSTEM can start/stop/reconfigure service. Authenticated Users limited to QueryStatus/QueryConfig/Interrogate - Vital="yes" on ServiceInstall Docs & roadmap: - PRODUCTION_ROADMAP.md: 5-milestone plan (security + features + distribution + ops) with granular tasks, effort, done-when - CLIENT_SECURITY_AUDIT.md, SECURITY_FIXES.md, DEPLOYMENT.md - AI assistant rules (.cursorrules, .antigravityrules, etc.) Build & distribution: - build-msi.ps1, deploy-and-sign.ps1, sign-release.ps1 - redeploy.ps1, tray-deploy.ps1, test-msi.ps1 - installer/msi/ alternative WXS setup - Restored .github/workflows/ removed in mirror cleanup .gitignore hardened: *.pfx, *.p12, *.key, *.pem, .env*
13 KiB
🔍 Multi-Agent Review: Audit Log Flow
Mục đích: Phản biện luồng Audit Log từ 4 góc nhìn chuyên gia, đảm bảo triển khai được cho cả API-only và Blazor Server model.
🤖 Agent 1: @backend-specialist — Architecture & Scalability
✅ Điểm mạnh
| Aspect | Assessment |
|---|---|
| Layered Architecture | Controller → Service → Repository pattern tuân thủ tốt |
| Async by default | Tất cả publisher/consumer đều async Task |
| Dependency Injection | Lifetimes đúng: Scoped (per-request), Singleton (stateless), HostedService (background) |
| Error Isolation | Audit log failure swallow exceptions, không break business flow |
| Message Broker Decoupling | RabbitMQ tách biệt hoàn toàn publisher/consumer |
🔴 CRITICAL: Gaps khi triển khai cho Blazor Server
Caution
IRequestContextAccessorKHÔNG hoạt động trong Blazor Server vì không có HTTP request pipeline!
| Problem | Blazor WASM (hiện tại) | Blazor Server | API-only |
|---|---|---|---|
IRequestContextAccessor |
✅ HTTP POST → Middleware populate | ❌ SignalR Hub, không có HttpContext | ✅ HTTP Request |
CorrelationIdMiddleware |
✅ HTTP headers | ❌ Không có HTTP headers | ✅ HTTP headers |
LogService POST to API |
✅ HttpClient call | ⚠️ Gọi API từ server → chính nó? Circular! | N/A |
INS_AuditLogger lấy auth |
✅ AuthenticationStateProvider |
✅ Works (nhưng flow khác) | N/A |
Giải pháp đề xuất cho Blazor Server:
// 1. Tạo ICircuitContextAccessor thay vì IRequestContextAccessor
public interface ICircuitContextAccessor
{
string? UserId { get; set; }
string? Username { get; set; }
string? IpAddress { get; set; }
string? SessionId { get; set; }
string? CorrelationId { get; set; }
}
// 2. Populate từ CircuitHandler thay vì middleware
public class AuditCircuitHandler : CircuitHandler
{
public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken ct)
{
// Lấy HttpContext từ initial connection
// HttpContext chỉ available tại thời điểm kết nối SignalR
}
}
// 3. Inject trực tiếp IAuditLogPublisher trong Blazor Server
// Không cần roundtrip qua HTTP/gRPC
@inject IAuditLogPublisher AuditPublisher
🟡 WARNING: Missing Pieces cho API-only Model
-
Không có Logging Controller trong tài liệu: Module Backend Controller nhận POST từ
LogServicenhưng KHÔNG được document. Cần document/api/logging/auditendpoint ở module backend. -
Thiếu Retry Policy cho Publisher:
AuditLogPublisher.PublishAsync()catch exception nhưng KHÔNG retry. Nếu RabbitMQ tạm unavailable → message bị mất.
// Đề xuất: Thêm local buffer hoặc retry policy
public class ResilientAuditLogPublisher : IAuditLogPublisher
{
// Option A: Polly retry
// Option B: In-memory queue + background flush
// Option C: Outbox pattern (ghi vào DB trước, background job publish)
}
- Consumer chỉ xử lý
AuditLogMessage: NhưngLoggingGrpcServicepublishRabbitMQAuditLogDto(khác model). Consumer cần handle cả 2 hoặc cần adapter thống nhất model trước khi publish.
🤖 Agent 2: @security-auditor — Security & OWASP
🔴 CRITICAL Findings
| # | Severity | Finding | OWASP | Remediation |
|---|---|---|---|---|
| S1 | CRITICAL | RollbackSqlGenerator build SQL bằng string concat |
A05 Injection | Rollback SQL chỉ dùng cho reference, KHÔNG BAO GIỜ auto-execute. Thêm warning comment. |
| S2 | HIGH | RequestBody được log raw |
A09 Logging | Nhạy cảm data (passwords, tokens) có thể bị log. Cần sanitize/redact sensitive fields trước khi publish. |
| S3 | HIGH | Không có authorization trên Consumer | A01 Access Control | HostedService chạy với full system permissions. Cần validate message source. |
| S4 | MEDIUM | OldData/NewData lưu raw JSON |
A04 Crypto | PII data (email, phone, address) lưu plaintext. Cần xem xét encryption at rest hoặc redaction policy. |
| S5 | MEDIUM | Không có rate limiting cho audit publish | A02 Misconfiguration | Malicious module có thể flood RabbitMQ queue. Cần throttle hoặc quota per module. |
Chi tiết Finding S1: SQL Injection Risk
// HIỆN TẠI: RollbackSqlGenerator.cs
private string FormatValue(JsonElement element)
{
return element.ValueKind switch
{
JsonValueKind.String => $"N'{EscapeSqlString(element.GetString() ?? "")}'",
// EscapeSqlString chỉ replace ' → ''
// KHÔNG đủ cho tất cả SQL injection vectors!
};
}
Warning
EscapeSqlStringchỉ escape single quotes. Các vectors khác như; DROP TABLEtrong numeric fields, hoặc Unicode tricks vẫn có thể exploit nếu rollback SQL được auto-execute.
Remediation:
- Rollback SQL chỉ dùng tham khảo (display-only) → Thêm
-- WARNING: Manual review required before execution - Nếu cần auto-execute → chuyển sang parameterized queries hoặc stored procedure
Chi tiết Finding S2: Sensitive Data Logging
// AuditLogPublisher.cs line 123
RequestBody = _requestContext.RequestBody, // ← RAW body!
Đề xuất: Thêm redaction middleware:
public static class SensitiveDataRedactor
{
private static readonly string[] SensitiveFields =
{ "password", "token", "secret", "creditCard", "ssn" };
public static string Redact(string? json) { /* redact matching fields */ }
}
🤖 Agent 3: @debugger — Failure Modes & Observability
Failure Mode Analysis (5 Whys)
Scenario: Audit log KHÔNG được ghi vào DB
WHY message không vào DB?
→ Consumer NACK (return false) hoặc message bị mất
WHY consumer NACK?
→ DbContext.SaveChangesAsync() throw exception (DB connection fail, constraint violation)
WHY DbContext fail?
→ Connection string sai, hoặc schema [log] chưa tạo, hoặc DB full
WHY không biết bị lỗi?
→ LogError chỉ log local, KHÔNG có alerting mechanism
WHY không có alerting?
→ CHƯA implement Dead Letter Queue (DLQ) hoặc health check cho consumer
🟡 Missing Observability
| Gap | Impact | Recommendation |
|---|---|---|
| Không có DLQ | NACK message bị requeue vô hạn → CPU spike | Thêm DLQ: ins.server.auditlog.dlq sau N retries |
| Không có metrics | Không biết queue depth, processing rate, failure rate | Export Prometheus metrics từ consumer |
| Consumer một chiều | Không biết consumer chạy hay không từ UI | Đã có /api/rabbitmq/consumer-status nhưng cần health check dashboard |
| Message ordering | Không guarantee order → UPDATE có thể đến trước INSERT | Chấp nhận được vì audit log timestamp-based, nhưng cần document |
| Duplicate messages | RabbitMQ at-least-once delivery → có thể duplicate INSERT | Cần idempotency check (correlation_id + timestamp + table + rowId) |
Troubleshooting Gaps trong tài liệu
Tài liệu thiếu các scenarios debugging phổ biến:
1. "Consumer connected nhưng không process message"
→ Kiểm tra binding key mismatch (logs.audit.server vs logs.audit.#)
→ Kiểm tra exchange type (topic vs direct)
2. "Message bị mất sau restart"
→ Queue có durable=true không?
→ Message có persistent=true không?
→ Kiểm tra IRabbitMQService default settings
3. "Blazor Server không ghi audit log"
→ IRequestContextAccessor null (không có HTTP context)
→ LogService gọi HTTP tới chính nó (circular dependency)
🤖 Agent 4: @frontend-specialist — Blazor Component & UX
Blazor Server vs WASM: Component Behavior
| Aspect | Blazor WASM | Blazor Server | Action Required |
|---|---|---|---|
LogService HTTP call |
Client → Server (OK) | Server → Server (❌ Circular) | Inject IAuditLogPublisher trực tiếp |
INS_AuditLogger rendering |
Component trên client | Component trên server | Hoạt động, nhưng UserId flow khác |
AuthenticationStateProvider |
Frontend auth state | Server auth state | ✅ Cả hai đều hoạt động |
| Error swallowing | User không thấy lỗi | User không thấy lỗi | ✅ Consistent |
| Serialization overhead | JSON serialize trên client | JSON serialize trên server | ✅ Cả hai đều OK |
🔴 CRITICAL: LogService Circular Call trong Blazor Server
Blazor Server:
INS_AuditLogger → LogService.AuditAsync()
→ HttpClient.PostAsync("/api/logging/audit")
→ Gọi tới CHÍNH SERVER ĐANG CHẠY
→ Self-referencing HTTP call → có thể deadlock hoặc connection exhaustion!
Giải pháp:
// Tạo IServerAuditLogger cho Blazor Server
public interface IServerAuditLogger
{
Task LogAsync(string tableName, string operation, string? rowId,
string? oldData, string? newData);
}
// Implementation inject trực tiếp IAuditLogPublisher (không qua HTTP)
public class ServerAuditLogger : IServerAuditLogger
{
private readonly IAuditLogPublisher _publisher;
private readonly AuthenticationStateProvider _authState;
public async Task LogAsync(string tableName, string operation,
string? rowId, string? oldData, string? newData)
{
var user = await GetCurrentUser();
await _publisher.PublishAsync("dbo", tableName, rowId ?? "",
operation, oldData, newData, functionName: "BlazorServer");
}
}
Component Registration Pattern
// Blazor WASM → dùng LogService (HTTP)
services.AddScoped<LogService>();
// Blazor Server → dùng ServerAuditLogger (direct inject)
services.AddScoped<IServerAuditLogger, ServerAuditLogger>();
// Shared → INS_AuditLogger cần abstraction
// Option: ILogServiceAbstraction với 2 implementations
📋 Tổng hợp: Bảng Action Items
| Priority | Finding | Agent | Model | Action |
|---|---|---|---|---|
| 🔴 P0 | IRequestContextAccessor không hoạt động Blazor Server |
Backend | Blazor Server | Tạo ICircuitContextAccessor + CircuitHandler |
| 🔴 P0 | LogService circular call trong Blazor Server |
Frontend | Blazor Server | Tạo IServerAuditLogger inject trực tiếp IAuditLogPublisher |
| 🔴 P0 | SQL Injection risk trong rollback SQL | Security | Cả hai | Mark rollback SQL là display-only, thêm warning |
| 🟡 P1 | Sensitive data logging (passwords, tokens) | Security | Cả hai | Thêm redaction trước khi publish |
| 🟡 P1 | Không có DLQ cho failed messages | Debugger | Cả hai | Configure DLQ + max retry count |
| 🟡 P1 | Message model mismatch (DTO vs full model) | Backend | Cả hai | Thống nhất hoặc adapter pattern |
| 🟡 P1 | Thiếu module logging controller doc | Backend | API | Document /api/logging/audit endpoint |
| 🟢 P2 | Không có duplicate detection | Debugger | Cả hai | Idempotency check trên consumer |
| 🟢 P2 | Thiếu metrics/observability | Debugger | Cả hai | Prometheus metrics cho queue depth |
| 🟢 P2 | Không có rate limiting cho publish | Security | API | Throttle per-module publish rate |
🏗️ Đề xuất: Abstraction Layer cho Multi-Model Support
Để audit log hoạt động trên cả API, Blazor WASM, và Blazor Server:
graph TD
subgraph "Blazor WASM"
A["INS_AuditLogger"] --> B["LogService<br/>(HTTP Client)"]
B --> C["Module Controller<br/>POST /api/logging/audit"]
C --> D["gRPC → LoggingGrpcService"]
end
subgraph "Blazor Server"
E["INS_AuditLogger"] --> F["IServerAuditLogger<br/>(Direct Inject)"]
F --> G["IAuditLogPublisher<br/>(Direct)"]
end
subgraph "API Only"
H["Controller Code"] --> G
end
D --> I["IRabbitMQService.SendAsync()"]
G --> I
I --> J["RabbitMQ<br/>ins.logs.exchange"]
J --> K["AuditLogConsumerService"]
K --> L["SQL Server<br/>[log].[audit_log]"]
Key abstraction: IAuditService interface cho cả 3 model
public interface IAuditService
{
Task LogAsync(string tableName, string operation, string? rowId,
object? oldData, object? newData, string? functionName = null);
}
// Blazor WASM implementation → HTTP → gRPC → RabbitMQ
public class WasmAuditService : IAuditService { }
// Blazor Server implementation → Direct → RabbitMQ
public class ServerAuditService : IAuditService { }
// API implementation → Direct → RabbitMQ
public class ApiAuditService : IAuditService { }
Registration per hosting model:
// Program.cs
if (isBlazorServer)
services.AddScoped<IAuditService, ServerAuditService>();
else if (isBlazorWasm)
services.AddScoped<IAuditService, WasmAuditService>();
else // API
services.AddScoped<IAuditService, ApiAuditService>();