In a docker compose solution, there are multiple ASP.NET Core projects. Each of them provide a service. Some of them can be accessed from internet (with REST APIs), some of them are for local docker network only (with gRPC APIs), and some of them have both public REST endpoints and private gRPC endpoints.
Only one project (gateway) which is a YARP proxy has published ports and this project will do the request forwarding.
For faster communications, the internal communications are not secured (since they will not face the internet directly).
Now those projects that have both public and private endpoints are configured in the gateway to accept requests, but the private gRPC endpoint should not be accessible to public.
I came up with two solutions:
- restrict access to these endpoint in the gateway with custom rules, or
- each project can protect itself, for example with a custom authorization that would limit the request to local network only
The first choice means that if I update the routes or naming of endpoints in a project I would have to update the gateway as well, which brings the risk of forgetting it or misconfigurations. So I went with the second choice.
The following is my custom authorization handler:
public class LocalNetworkHandler : IAuthorizationRequirement, IAuthorizationHandler
{
public const string PolicyName = "LocalNetwork";
private readonly IpAddressRange[] privateIpRanges =
{
new IpAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("10.255.255.255")),
new IpAddressRange(IPAddress.Parse("172.16.0.0"), IPAddress.Parse("172.31.255.255")),
new IpAddressRange(IPAddress.Parse("192.168.0.0"), IPAddress.Parse("192.168.255.255")),
};
public Task HandleAsync(AuthorizationHandlerContext context)
{
var httpContext = context.Resource as HttpContext;
if (httpContext is null) return Task.CompletedTask;
var ipAddress = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
var isPrivateIp = privateIpRanges.Any(range => range.Contains(ipAddress));
if (isPrivateIp) context.Succeed(this);
else context.Fail();
return Task.CompletedTask;
}
private class IpAddressRange
{
private uint startIpInt;
private uint endIpInt;
public IpAddressRange(IPAddress start, IPAddress end)
{
startIpInt = BitConverter.ToUInt32(start.GetAddressBytes().Reverse().ToArray(), 0);
endIpInt = BitConverter.ToUInt32(end.GetAddressBytes().Reverse().ToArray(), 0);
}
public bool Contains(IPAddress ipAddress)
{
var ipInt = BitConverter.ToUInt32(ipAddress.GetAddressBytes().Reverse().ToArray(), 0);
return ipInt >= startIpInt && ipInt <= endIpInt;
}
}
}
The IP range logics are written with the help of this answer.
And this is how I use it:
var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(
LocalNetworkHandler.PolicyName,
policy => policy.Requirements.Add(new LocalNetworkHandler()));
});
...
app.UseAuthorization();
app.MapControllers();
app.MapGrpcService<MyRpcService>()
.RequireAuthorization(LocalNetworkHandler.PolicyName);
app.Run();
1 Answer 1
privateIpRanges
should be cached, and it should be a thread-safe collection as well.- the
IpAddressRange
would be better if it's constructed outside theLocalNetworkHandler
, or you can use Tuple as replacement. IPAddress
conversion touint
can be used as an extension method or a private method.- setting up the
privateIpRanges
elements should be done outside the handler to make it easier to adjust the IPs without updating the handler. - there is an active maintained library
IPAddressRange
that would eliminate the need ofIpAddressRange
and its conversions I highly recommend it. - construct an
IServiceCollection
extension method that would configure theLocalNetworkHandler
this would give your startup a better readable and extensible api, and give your code a freedom to expand and maintain.
The following examples used IPAddressRange
library. For the sake of simplicity, I have tried to minimize the work.
LocalNetworkService
this is a service which has Default
instance, the default instance would be configured at startup.
public class LocalNetworkService
{
private readonly ConcurrentBag<IPAddressRange> _privateIPAddresses = new ConcurrentBag<IPAddressRange>();
public static readonly LocalNetworkService Default = new LocalNetworkService();
public LocalNetworkService() { }
public void AddIPAddressRange(IPAddressRange address)
{
if(address == null)
throw new ArgumentNullException(nameof(address));
_privateIPAddresses.Add(address);
}
public void AddIPAddressRange(IEnumerable<IPAddressRange> addresses)
{
if (addresses?.Any() == false)
throw new ArgumentNullException(nameof(addresses));
foreach (var address in addresses)
_privateIPAddresses.Add(address);
}
// if empty, it means there is no restrictions
public bool IsPrivateAddress(IPAddress address) => _privateIPAddresses.IsEmpty || _privateIPAddresses.Any(x => x.Contains(address));
}
LocalNetworkHandler
LocalNetworkHandler
does not tell us what type of handler is it, so renaming it to LocalNetworkAuthorizationHandler
would give a better understanding on the class purpose.
public class LocalNetworkAuthorizationHandler : IAuthorizationRequirement, IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
var httpContext = context.Resource as HttpContext;
if (httpContext is null) return Task.CompletedTask;
var ipAddress = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
if (LocalNetworkService.Default.IsPrivateAddress(ipAddress))
{
context.Succeed(this);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
LocalNetworkServiceCollectionExtension
A simple IServiceCollection
extension method that would setup the service with a readable and easy to configure methods.
public static class LocalNetworkServiceCollectionExtension
{
private const string LocalNetworkPolicyName = "LocalNetwork";
private static IServiceCollection InternalAddLocalNetworkAuthorization(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy(LocalNetworkPolicyName,
policy => policy.Requirements.Add(new LocalNetworkAuthorizationHandler())
);
});
return services;
}
public static IServiceCollection AddLocalNetworkAuthorization(this IServiceCollection services, params IPAddressRange[] addresses)
{
if (addresses?.Length > 0)
LocalNetworkService.Default.AddIPAddressRange(addresses);
return InternalAddLocalNetworkAuthorization(services);
}
public static IServiceCollection AddLocalNetworkAuthorization(this IServiceCollection services, params string[] addresses)
{
if (addresses?.Length > 0)
{
foreach (var address in addresses)
{
if(IPAddressRange.TryParse(address, out IPAddressRange range))
{
LocalNetworkService.Default.AddIPAddressRange(range);
}
}
}
return InternalAddLocalNetworkAuthorization(services);
}
}
Startup
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLocalNetworkAuthorization("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16");
I know it needs more work, but I think it's enough to give you a better view on how things going to go from there.
-
\$\begingroup\$ To my knowledge a read-only/constant field is thread-safe by definition. It will never change so threads are free to access it without any synchronization. \$\endgroup\$Parsa99– Parsa992022年07月29日 14:44:04 +00:00Commented Jul 29, 2022 at 14:44
-
\$\begingroup\$ @Parsa99 that's correct. if you don't plan to update the values from external sources or other classes, then your array would be more than enough to get the job done. \$\endgroup\$iSR5– iSR52022年07月29日 17:28:27 +00:00Commented Jul 29, 2022 at 17:28
Explore related questions
See similar questions with these tags.
LocalNetworkHandler
would be implemented ? is it on YARP (gateway) or is it going to be a separated project ? \$\endgroup\$