Security isn't optional. Whether you're building internal microservices or client-facing applications, protecting your RPC traffic is essential. WitRPC provides built-in security features that are both powerful and easy to use.
Today, we'll explore WitRPC's security capabilities: encryption for data protection, authentication for access control, and best practices for production deployments.
The Security Landscape
When data travels between client and server, it faces several threats:
| Threat | Description | Mitigation |
|---|---|---|
| Eavesdropping | Attackers read your traffic | Encryption |
| Man-in-the-Middle | Attackers intercept and modify traffic | Encryption + TLS |
| Unauthorized Access | Unpermitted clients connect | Authentication |
| Replay Attacks | Attackers resend captured requests | Session tokens |
| Data Tampering | Attackers modify messages | Encryption (integrity) |
WitRPC addresses these through two complementary mechanisms:
- Encryption: Protects data in transit
- Authentication: Controls who can connect
Let's explore each in detail.
Encryption: Protecting Data in Transit
WitRPC offers end-to-end encryption that protects all communication between client and server.
How WitRPC Encryption Works
WitRPC uses a hybrid encryption scheme:
Technical details:
- Key Exchange: RSA-OAEP with SHA-256, 2048-bit keys
- Data Encryption: AES-256-CBC with PKCS7 padding
- Per-Session Keys: Fresh AES key generated for each connection
Enabling Encryption
Encryption is enabled with a single method call, and must be enabled on both sides:
// Server
var server = WitServerBuilder.Build(options =>
{
options.WithService(new MyService());
options.WithTcp(5000, maxNumberOfClients: 100);
options.WithMessagePack();
options.WithEncryption(); // Enable encryption
});
// Client
var client = WitClientBuilder.Build(options =>
{
options.WithTcp("server.example.com", 5000);
options.WithMessagePack();
options.WithEncryption(); // Must match server!
});Important: Both client and server must use the same encryption setting. Mismatched settings cause connection failures.
| Server | Client | Result |
|---|---|---|
WithEncryption() |
WithEncryption() |
✅ Works |
WithEncryption() |
WithoutEncryption() |
❌ Fails |
WithoutEncryption() |
WithEncryption() |
❌ Fails |
WithoutEncryption() |
WithoutEncryption() |
✅ Works (insecure) |
BouncyCastle: Cross-Platform Encryption
Standard .NET cryptography doesn't work everywhere. Blazor WebAssembly, in particular, lacks access to some cryptographic primitives. WitRPC solves this with BouncyCastle-based encryption.
Install the packages:
# Server
dotnet add package OutWit.Communication.Server.Encryption.BouncyCastle
# Client (Blazor WebAssembly)
dotnet add package OutWit.Communication.Client.Encryption.BouncyCastleUse BouncyCastle encryption:
// Server (must use BouncyCastle if client does)
var server = WitServerBuilder.Build(options =>
{
options.WithService(new MyService());
options.WithWebSocket("http://0.0.0.0:5000", maxNumberOfClients: 100);
options.WithJson();
options.WithBouncyCastleEncryption(); // BouncyCastle on server
});
// Client (Blazor WebAssembly)
var client = WitClientBuilder.Build(options =>
{
options.WithWebSocket("ws://server.example.com:5000");
options.WithJson();
options.WithBouncyCastleEncryption(); // BouncyCastle on client
});Compatibility matrix:
| Client | Server | Compatible? |
|---|---|---|
WithBouncyCastleEncryption() |
WithBouncyCastleEncryption() |
✅ Yes |
WithBouncyCastleEncryption() |
WithEncryption() |
❌ No |
WithEncryption() |
WithEncryption() |
✅ Yes |
WithEncryption() |
WithBouncyCastleEncryption() |
❌ No |
When to use BouncyCastle:
- Blazor WebAssembly clients
- Cross-platform consistency requirements
- Environments where .NET crypto is unavailable
When to use standard encryption:
- Desktop applications
- Server-to-server communication
- Standard .NET environments
TLS for TCP: Transport-Level Security
For TCP connections, you can also use TLS (Transport Layer Security) for an additional layer of protection:
// Server with TLS
var certificate = new X509Certificate2("server.pfx", "password");
var server = WitServerBuilder.Build(options =>
{
options.WithService(new MyService());
options.WithTcpSecure(5000, maxNumberOfClients: 100, certificate);
options.WithMessagePack();
// WitRPC encryption is optional with TLS, but adds defense-in-depth
options.WithEncryption();
});
// Client connecting to TLS endpoint
var client = WitClientBuilder.Build(options =>
{
options.WithTcpSecure("server.example.com", 5000, "server.example.com", sslValidationCallback: null);
options.WithMessagePack();
options.WithEncryption();
});TLS vs WitRPC Encryption:
| Feature | TLS | WitRPC Encryption |
|---|---|---|
| Certificate required | Yes | No |
| Works with all transports | TCP only | All transports |
| Industry standard | Yes | Proprietary |
| Mutual authentication | Possible | No |
| End-to-end encryption | To TLS termination | True end-to-end |
Recommendation: For maximum security, use both TLS (transport) and WitRPC encryption (application). This provides defense-in-depth.
Authentication: Controlling Access
Encryption protects data, but authentication controls who can connect. WitRPC supports token-based authentication.
Simple Token Authentication
The simplest authentication method is a shared access token:
// Server
var server = WitServerBuilder.Build(options =>
{
options.WithService(new MyService());
options.WithTcp(5000, maxNumberOfClients: 100);
options.WithMessagePack();
options.WithEncryption();
options.WithAccessToken("MySecretToken123!"); // Require this token
});
// Client
var client = WitClientBuilder.Build(options =>
{
options.WithTcp("server.example.com", 5000);
options.WithMessagePack();
options.WithEncryption();
options.WithAccessToken("MySecretToken123!"); // Provide the token
});If the tokens don't match, the connection is rejected:
try
{
await client.ConnectAsync(TimeSpan.FromSeconds(5));
}
catch (WitAuthorizationException ex)
{
Console.WriteLine("Authentication failed: " + ex.Message);
}Token from Configuration
Don't hardcode tokens! Load them from configuration:
// appsettings.json
{
"WitRpc": {
"AccessToken": "MySecretToken123!"
}
}
// Server
var token = configuration["WitRpc:AccessToken"];
options.WithAccessToken(token);
// Or use environment variables
var token = Environment.GetEnvironmentVariable("WITRPC_TOKEN");
options.WithAccessToken(token);Custom Token Validation
For more sophisticated authentication, implement a custom token validator:
public class JwtTokenValidator : IAccessTokenValidator
{
private readonly IConfiguration _configuration;
private readonly ILogger<JwtTokenValidator> _logger;
public JwtTokenValidator(
IConfiguration configuration,
ILogger<JwtTokenValidator> logger)
{
_configuration = configuration;
_logger = logger;
}
// IAccessTokenValidator has two checks: one for the connection
// handshake, one for individual requests. Here both apply the
// same JWT validation.
public bool IsAuthorizationTokenValid(string token) => Validate(token);
public bool IsRequestTokenValid(string token) => Validate(token);
private bool Validate(string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_configuration["Jwt:Secret"]);
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = _configuration["Jwt:Issuer"],
ValidAudience = _configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(key)
}, out _);
_logger.LogInformation("Token validated successfully");
return true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Token validation failed");
return false;
}
}
}
// Register with server
var server = WitServerBuilder.Build(options =>
{
options.WithService(new MyService());
options.WithTcp(5000, maxNumberOfClients: 100);
options.WithEncryption();
options.WithAccessTokenValidator(new JwtTokenValidator(configuration, logger));
});API Key Authentication
For service-to-service communication, API keys are common:
public class ApiKeyValidator : IAccessTokenValidator
{
private readonly HashSet<string> _validKeys;
private readonly ILogger<ApiKeyValidator> _logger;
public ApiKeyValidator(IConfiguration config, ILogger<ApiKeyValidator> logger)
{
_logger = logger;
// Load valid API keys from configuration
_validKeys = config.GetSection("ApiKeys")
.Get<string[]>()
?.ToHashSet() ?? new HashSet<string>();
}
public bool IsAuthorizationTokenValid(string token) => Validate(token);
public bool IsRequestTokenValid(string token) => Validate(token);
private bool Validate(string token)
{
var isValid = _validKeys.Contains(token);
if (!isValid)
_logger.LogWarning("Invalid API key attempted");
return isValid;
}
}Database-Backed Authentication
For dynamic token management:
public class DatabaseTokenValidator : IAccessTokenValidator
{
private readonly IDbConnection _db;
private readonly ILogger<DatabaseTokenValidator> _logger;
public bool IsAuthorizationTokenValid(string token) => Validate(token);
public bool IsRequestTokenValid(string token) => Validate(token);
// The validator interface is synchronous, so database checks run
// synchronously here. In high-throughput systems, back this with an
// in-memory cache of active tokens refreshed in the background, so the
// hot path never waits on the database.
private bool Validate(string token)
{
var apiKey = _db.QueryFirstOrDefault<ApiKey>(
"SELECT * FROM ApiKeys WHERE Token = @Token AND IsActive = 1 AND ExpiresAt > @Now",
new { Token = token, Now = DateTime.UtcNow });
if (apiKey == null)
{
_logger.LogWarning("Token not found or expired");
return false;
}
_logger.LogInformation("Token validated for client {ClientId}", apiKey.ClientId);
return true;
}
}Combining Encryption and Authentication
For production systems, use both encryption and authentication together:
// Production server configuration
var server = WitServerBuilder.Build(options =>
{
// Services
options.WithServices()
.AddService<IUserService>(userService)
.AddService<IOrderService>(orderService)
.Build();
// Transport with TLS
var certificate = LoadCertificate();
options.WithTcpSecure(5000, maxNumberOfClients: 200, certificate);
// Serialization
options.WithMessagePack();
// Application-level encryption (defense-in-depth)
options.WithEncryption();
// Authentication
options.WithAccessTokenValidator(new JwtTokenValidator(config, logger));
});
// Production client configuration
var client = WitClientBuilder.Build(options =>
{
options.WithTcpSecure("api.example.com", 5000);
options.WithMessagePack();
options.WithEncryption();
options.WithAccessToken(await GetJwtTokenAsync());
// Resilience
options.WithAutoReconnect(reconnect =>
{
reconnect.MaxAttempts = 10;
reconnect.OnReconnected = async () =>
{
// Refresh token on reconnection
var newToken = await GetJwtTokenAsync();
// Token is automatically used for reconnection
};
});
});Security for Different Scenarios
Scenario 1: Local IPC (Same Machine)
For same-machine communication in trusted environments:
// Lower security requirements: processes on same machine
var server = WitServerBuilder.Build(options =>
{
options.WithService(new MyService());
options.WithNamedPipe("MyApp-Secure");
options.WithMemoryPack();
// Still use encryption to prevent other local processes from reading
options.WithEncryption();
// Simple token for basic access control
options.WithAccessToken(Environment.GetEnvironmentVariable("IPC_TOKEN"));
});Scenario 2: Internal Microservices
For services within a private network:
// Internal network: moderate security
var server = WitServerBuilder.Build(options =>
{
options.WithService(new MyService());
options.WithTcp(5000, maxNumberOfClients: 100);
options.WithMessagePack();
// Encryption for data protection
options.WithEncryption();
// API key authentication
options.WithAccessTokenValidator(new ApiKeyValidator(config));
});Scenario 3: Public-Facing Service
For services exposed to the internet:
// Public internet: maximum security
var certificate = new X509Certificate2("production.pfx", certPassword);
var server = WitServerBuilder.Build(options =>
{
options.WithService(new MyService());
// TLS for transport security
options.WithTcpSecure(443, maxNumberOfClients: 1000, certificate);
options.WithMessagePack();
// Application encryption (defense-in-depth)
options.WithEncryption();
// JWT authentication with short-lived tokens
options.WithAccessTokenValidator(new JwtTokenValidator(config));
});Scenario 4: Blazor WebAssembly
For browser-based clients:
// Server
var server = WitServerBuilder.Build(options =>
{
options.WithService(new MyService());
options.WithWebSocket("https://0.0.0.0:443", maxNumberOfClients: 500);
options.WithJson(); // Or MessagePack
// BouncyCastle for Blazor compatibility
options.WithBouncyCastleEncryption();
// JWT from browser's auth flow
options.WithAccessTokenValidator(new JwtTokenValidator(config));
});
// Blazor Client
var client = WitClientBuilder.Build(options =>
{
options.WithWebSocket("wss://api.example.com");
options.WithJson();
options.WithBouncyCastleEncryption(); // Required for WASM
options.WithAccessToken(await authService.GetAccessTokenAsync());
});Security Best Practices
1. Always Use Encryption Over Networks
// Bad: unencrypted network communication
options.WithTcp("server", 5000);
options.WithoutEncryption(); // Dangerous!
// Good: always encrypt
options.WithTcp("server", 5000);
options.WithEncryption();2. Rotate Tokens Regularly
public class TokenRotationService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Refresh token before expiration
var newToken = await _authService.RefreshTokenAsync();
_client.UpdateAccessToken(newToken);
// Wait until close to expiration
await Task.Delay(TimeSpan.FromMinutes(55), stoppingToken);
}
}
}3. Use Strong Tokens
// Bad: weak token
options.WithAccessToken("password123");
// Good: strong random token
var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
options.WithAccessToken(token);
// Better: JWT with claims and expiration
var jwt = GenerateJwtToken(userId, roles, TimeSpan.FromHours(1));
options.WithAccessToken(jwt);4. Log Security Events
public class AuditingTokenValidator : IAccessTokenValidator
{
private readonly IAccessTokenValidator _innerValidator;
public bool IsAuthorizationTokenValid(string token)
{
var result = _innerValidator.IsAuthorizationTokenValid(token);
// Log all authentication attempts (fire-and-forget)
_auditLog.Log(new AuthenticationEvent
{
Timestamp = DateTime.UtcNow,
TokenHash = ComputeHash(token), // Don't log the actual token!
Success = result
});
return result;
}
public bool IsRequestTokenValid(string token) =>
_innerValidator.IsRequestTokenValid(token);
}5. Secure Token Storage
// Bad: token in code
var token = "hardcoded-secret";
// Bad: token in appsettings.json (committed to git)
var token = config["Token"];
// Good: environment variable
var token = Environment.GetEnvironmentVariable("WITRPC_TOKEN");
// Better: secret manager (Azure Key Vault, AWS Secrets Manager, etc.)
var token = await secretClient.GetSecretAsync("witrpc-token");6. Validate on Every Request
WitRPC validates tokens on connection. For long-lived connections, consider additional validation:
public class SecureOrderService : IOrderService
{
private readonly IAuthorizationService _auth;
public async Task<Order> GetOrderAsync(int orderId)
{
// Additional authorization check per request
var user = await _auth.GetCurrentUserAsync();
if (!await _auth.CanAccessOrderAsync(user, orderId))
{
throw new UnauthorizedAccessException();
}
return await _repository.GetOrderAsync(orderId);
}
}Troubleshooting Security Issues
Connection Rejected (Encryption Mismatch)
Symptom: Client fails to connect with cryptic error.
Cause: Client and server have different encryption settings.
Solution: Ensure both sides use matching encryption:
// Both must match!
server: options.WithEncryption();
client: options.WithEncryption();Authentication Failed
Symptom: WitAuthorizationException thrown.
Cause: Token doesn't match or is invalid.
Solution:
- Verify token is correct on both sides
- Check token hasn't expired
- Ensure token validator is working correctly
BouncyCastle Compatibility
Symptom: Blazor client can't connect to server.
Cause: Server uses standard encryption, client uses BouncyCastle.
Solution: Both must use BouncyCastle:
server: options.WithBouncyCastleEncryption();
client: options.WithBouncyCastleEncryption();Conclusion
WitRPC provides comprehensive security features:
- Encryption: AES-256 with RSA key exchange, plus BouncyCastle for cross-platform
- TLS: Transport-level security for TCP connections
- Authentication: Simple tokens, custom validators, JWT support
- Defense-in-depth: Multiple layers of protection
Key takeaways:
- Always encrypt network traffic
- Use authentication for access control
- Match encryption settings on client and server
- Use BouncyCastle for Blazor WebAssembly
- Store tokens securely, rotate regularly
- Log security events for audit trails
Security is a journey, not a destination. Start with these fundamentals, and enhance based on your threat model.
Next up: Deploying WitRPC Services to Production, the operational side: configuration, containers, logging, and monitoring.
This is part 11 of a series on WitRPC. See previous posts: Introducing WitRPC, Getting Started, Real-World Benefits, WitRPC vs. gRPC, Migrating to WitRPC, Under the Hood, Composite Services, ASP.NET Core Integration, Performance Tuning, and Resilience.