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:

  1. Encryption: Protects data in transit
  2. 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:

csharp
// 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:

bash
# Server
dotnet add package OutWit.Communication.Server.Encryption.BouncyCastle

# Client (Blazor WebAssembly)
dotnet add package OutWit.Communication.Client.Encryption.BouncyCastle

Use BouncyCastle encryption:

csharp
// 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:

csharp
// 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:

csharp
// 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:

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

csharp
// 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:

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

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

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

csharp
// 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:

csharp
// 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:

csharp
// 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:

csharp
// 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:

csharp
// 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

csharp
// Bad: unencrypted network communication
options.WithTcp("server", 5000);
options.WithoutEncryption();  // Dangerous!

// Good: always encrypt
options.WithTcp("server", 5000);
options.WithEncryption();

2. Rotate Tokens Regularly

csharp
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

csharp
// 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

csharp
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

csharp
// 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:

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

csharp
// Both must match!
server: options.WithEncryption();
client: options.WithEncryption();

Authentication Failed

Symptom: WitAuthorizationException thrown.

Cause: Token doesn't match or is invalid.

Solution:

  1. Verify token is correct on both sides
  2. Check token hasn't expired
  3. 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:

csharp
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.