1
\$\begingroup\$

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

enter image description here

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:

  1. restrict access to these endpoint in the gateway with custom rules, or
  2. 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();
asked Jul 26, 2022 at 19:34
\$\endgroup\$
3
  • \$\begingroup\$ would you mind clarify where LocalNetworkHandler would be implemented ? is it on YARP (gateway) or is it going to be a separated project ? \$\endgroup\$ Commented Jul 28, 2022 at 14:30
  • \$\begingroup\$ A shared library project \$\endgroup\$ Commented Jul 29, 2022 at 5:20
  • \$\begingroup\$ your question has multiple concerns, I suggest to move your security concerns into Information Security, and for the software design, Software Engineering would give you a better answer. I will post my answer on the code part only. \$\endgroup\$ Commented Jul 29, 2022 at 7:12

1 Answer 1

1
\$\begingroup\$
  • privateIpRanges should be cached, and it should be a thread-safe collection as well.
  • the IpAddressRange would be better if it's constructed outside the LocalNetworkHandler, or you can use Tuple as replacement.
  • IPAddress conversion to uint 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 of IpAddressRange and its conversions I highly recommend it.
  • construct an IServiceCollection extension method that would configure the LocalNetworkHandler 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.

answered Jul 29, 2022 at 13:13
\$\endgroup\$
2
  • \$\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\$ Commented 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\$ Commented Jul 29, 2022 at 17:28

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.