netjail

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 1, 2024 License: MIT Imports: 9 Imported by: 0

README

Build Go Report Card Go Reference MIT License

netjail

Go library providing network access controls for dial functions and http transports

Motivation

Modern production systems are becoming increasingly complex, and often need to perform actions that are controlled by user input. For example, users may be given the option to define a Webhook URL to send data to, sometimes as part of a larger workflow. Applications that give users the ability to control network requests are exposed to the security risks of seeing those functionalities exploited to perform unintended actions. Attackers can use these features to forge network requests to internal systems, either to access private information or perform actions they should not be permitted to.

Classic network access controls are effective measures to lock down access to protected systems; however, they are not enough.

Consider the case of deploying an application as an AWS Lambda Function. Lambda offers very strong isolation guarantees thanks to Firecracker, and the execution model guaranteeing that each instance of the function serves at most one concurrent invocation. However, the protocol used by AWS Lambda to receive function invocations uses a long-poll http request to a localhost endpoint. Security groups implemented by the network cannot prevent the program from connecting to the loopback interface, and a malicious input could forge requests to the local endpoint that the program was not supposed to make.

Lambda is one example, but there are many others, like an application invoking itself on unprotected internal endpoints, or sending requests to sidecar containers. Programs that have the ability to open connections to addresses extracted from user input must implement protections that the network layer cannot provide, which is what this package solves for.

Installation

This package is a library, it is intended to be used as a dependency in another application:

go get github.com/stealthrocket/netjail

Usage

The library covers tow main use cases: controlling network access at the HTTP and TCP layers.

Programs can import the package with:

import "github.com/stealthrocket/netjail"
Declaring rules for network access controls

Programs configure the set of networks that the application can connect to by declaring lists of allowed and blocked network ranges. An empty rule set denies access to all networks. The list of allowed prefixes opens access to networks, and subnets can be restricted further by the list of blocked prefixes.

This example shows how to declare rules that allow connecting to all networks except loopback interfaces:

allIPv4 := netip.MustParsePrefix("0.0.0.0/0")
allIPv6 := netip.MustParsePrefix("::/0")

localhostIPv4 := netip.MustParsePrefix("127.0.0.0/8")
localhostIPv6 := netip.MustParsePrefix("::1/128")

rules := &netjail.Rules{
    Allow: []netip.Prefix{
        allIPv4,
        allIPv6,
    },
    Block: []netip.Prefix{
        localhostIPv4,
        localhostIPv6,
    },
}

Network access controls often need to be propagated through the call stack of an application, which can be done by embedding the rules in a context.Context using this function:

ctx = netjail.ContextWithRules(ctx, rules)

The rules can later be retrieved by this function:

rules := netjail.ContextRules(ctx)

If the context does not contain any rules, the returned value is nil, which behaves like an empty rule set, and denies all network access. This is critical to fail closed in the presence of application misconfiguration or errors.

Network access controls for HTTP transports

The package provides an implementation of http.RoundTripper that extracts the set of network access control rules from the context of an http.Request, and applies the rules to the connection used to serve the request.

The HTTP transport works by dynamically creating instances of http.Transport, and overriding the dial function to apply network access controls.

The following examples shows how to construct a secured client that applies a set of rules to the requests it serves:

secureClient := &http.Client{
    Transport: &netjail.Transport{
        New: func() *http.Transport{
            // We must return a different instance each time the
            // function is called, the transport would otherwise
            // panic if it sees the same value more than once.
            return http.DefaultTransport.(*http.Transport).Clone()
        },
    },
}

...

// The context used to construct the request contains the network access
// control rules that will be applied when passed to the client.
ctx = netjail.ContextWithRules(ctx, &netjail.Rules{
    ...
})

req, err := http.NewRequestWithContext(ctx, "GET", userProvidedURL, nil)
...

res, err := secureClient.Do(req)
if err != nil {
    if errors.Is(err, netjail.ErrDenied) {
        // The request was intended for a forbidden address
        ...
    }
    ...
}
Network access controls for TCP connections

While most applications use HTTP or protocols that are based on HTTP, some may need to apply network access controls to clients of other protocols (e.g., MySQL, Redis, Kafka, etc...).

Libraries that provide clients for these protocols will often allow configuring a dial function to customize how network connections are opened. This dial function is the bottom-most hook that the netjail package can integrate with to apply security rules to any client.

The netjail.(*Rules).DialFunc method is the low-level wrapper that decorates dial functions to apply network access controls defined in the originating rules.

Security Considerations

Protection against DNS rebinding attacks

DNS rebinding attacks occur when an attacker attempts to forge the DNS responses received by an application, to route connections to addresses of their choosing. For example, they could cause the DNS resolution for example.com to resolve to 127.0.0.1, resulting in the application unknowningly connecting to localhost instead of a remote server.

To prevent these types of attacks, the netjail package takes over the entire connection process. It first resolves hostnames to a set of network addresses, then validates those addresses against the allow and block lists, and only opens connection to an address that passed the checks. With this process, there is no risk of confusion between the validation and connection phases, effectively protecting the application against DNS rebinding attacks that would attempt to bypass network access controls.

Protection against software defects and supplychain attacks

Software security threats come in many forms, each dependency of a program can become the target of an attacker, and beyond that, even the process by which the software is built and delivered can become the target of attackers. Producing secure software requires the combination of multiple mitigation strategies to defend against the variety of risks that the system is exposed to.

To limit exposure to those risks, the netjail package is designed to have no dependencies besides the Go standard library, and will remain dependency-free. The package has a well-defined scope, if new features are added, it will always be preferable to bring the code in-house than take a dependency on a third party.

We used design decisions that reduce the risk of seeing security exploits arise in buggy software. We limit the use of interface types, because abstractions make it more difficult to validate how the software will behave when components are replaced at run time. The use of concrete types offer stronger guarantees, and reduce the scope that tests need to cover. In addition, the library applies extensive validations against the inputs that it receives, and always defaults to failing closed or failing loud (e.g., blocking network connections, panicking in the presence of invalid state).

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrDenied is an error returned by a dial function when the address is
	// denied by security rules.
	ErrDenied = errors.New("address not allowed")
)

Functions

func ContextWithRules

func ContextWithRules(ctx context.Context, rules *Rules) context.Context

ContextWithRules returns a context which embeds the given network access control rules.

Types

type DialFunc

type DialFunc func(context.Context, string, string) (net.Conn, error)

DialFunc is a type of function used to establish network connections.

The function matches the signatures of standard functions like net.(*Dialer).DialContext or http.(*Transport).DialContext.

type Resolver

type Resolver interface {
	LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
}

Resolver is an interface used to abstract the name resolver used by security rules to convert logical hostnames to IP addresses.

type Rules

type Rules struct {
	Allow []netip.Prefix
	Block []netip.Prefix
}

Rules is a set of rules used to determine whether a network address can be accessed.

By default, the rules denies all addresses. Rules can be added to open networks, and further block subsets of the open address space.

func ContextRules

func ContextRules(ctx context.Context) *Rules

ContextRules returns the network access control rules embedded in ctx.

If the context did not contain any rules, nil is returned.

func (*Rules) Accept

func (rules *Rules) Accept(addr netip.Addr) bool

Accept returns true if the given address is allowed by the security rules.

func (*Rules) AppendTo

func (rules *Rules) AppendTo(data []byte) []byte

AppendTo appends a string representation of the security rules to the given byte slice and returns the resulting slice.

func (*Rules) Clone

func (rules *Rules) Clone() *Rules

Clone returns a deep copy of the security rules.

func (*Rules) DialFunc

func (rules *Rules) DialFunc(rslv Resolver, dial DialFunc) DialFunc

DialFunc returns a dial function using the given resolver and dialer to establish connections to addresses that are allowed by the security rules.

The resolver is used to convert logical hostnames to IP addresses before applying the security rules.

If the resolver is nil, net.DefaultResolver is used.

If the dialer is nil, a new dialer is created with the default options.

func (*Rules) String

func (rules *Rules) String() string

String returns a string representation of the security rules.

type Transport

type Transport struct {
	// A function used to create http transports for each network access control
	// configuration.
	//
	// The function must create a new http.Transport instance, and return it.
	// A panic is triggered if the function returns nil, or if it returns the
	// same http.Transport more than once.
	//
	// The returned http.Transport cannot have DialTLS or DialTLSContext set,
	// or a panic is triggered. This is due to network access controls having to
	// be applied before the TLS handshake on the IP addresses resolved from the
	// hostname in the request, but DialTLS and DialTSLContext need to receive
	// the hostname to validate the server certificate, which couples the
	// function to name resolution.
	//
	// A simple implementation of this function is to close the default http
	// transport:
	//
	//	New: func() *http.Transport {
	//		return http.DefaultTransport.(*http.Transport).Clone()
	//	}
	//
	// The function might be invoked concurrently from multiple goroutines.
	New func() *http.Transport

	// The resolver used to convert logical hostnames to IP addresses before
	// checking network access controls.
	//
	// If nil, the default resolver is used.
	Resolver Resolver

	// Maximum number of idle transports to retain. If the limit is reached,
	// the least recently used transport is evicted, and CloseIdleConnections
	// called.
	//
	// Default to 256.
	MaxIdleTransports int
	// contains filtered or unexported fields
}

Transport is a type similar to http.Transport, but which applies rules for network access control embedded in the context of requests it serves.

Requests that don't include network access control rules are always denied.

Requests that are denied fail with the error ErrDenied.

The implementation of this http transport uses http.Transport instances rather than http.RoundTripper. This design decision helps to limit the potential edge cases that may arise if applications are allowed to inject arbitrary http.RoundTripper instances into the transport. Since the purpose of this transport is to apply security controls, we want to optimize for safety, at the expense of composability in this case. Applications that need a more flexible integration can use the Rules type directly, by wrapping the dialers to implement network access controls.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net/http"
	"net/netip"

	"github.com/stealthrocket/netjail"
)

func main() {
	// This rule set only allows connecting to a private IPv4 network.
	ctx := netjail.ContextWithRules(context.Background(),
		&netjail.Rules{
			Allow: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/8"),
			},
		},
	)

	client := &http.Client{
		Transport: &netjail.Transport{
			New: func() *http.Transport {
				return http.DefaultTransport.(*http.Transport).Clone()
			},
		},
	}

	r, err := http.NewRequestWithContext(ctx, "GET", "http://localhost/", nil)
	if err != nil {
		log.Fatal(err)
	}

	if r, err := client.Do(r); err != nil {
		if !errors.Is(err, netjail.ErrDenied) {
			log.Fatal(err)
		} else {
			fmt.Println("Access Denied")
		}
	} else {
		r.Body.Close()
		fmt.Println("Access Granted")
	}

}
Output:

Access Denied

func (*Transport) CloseIdleConnections

func (t *Transport) CloseIdleConnections()

func (*Transport) RoundTrip

func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL