sniproxy

package module
v1.0.11 Latest Latest
Warning

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

Go to latest
Published: Nov 14, 2023 License: MIT Imports: 31 Imported by: 0

README

sniproxy

Build Status

SNI respecting TLS reverse proxy that has support for pluggable authentication. Built on top of the TLS termination sophistication and SNI capabilities of SillyProxy. Allows to make use of one's own authentication mechanisms, authorization checks, session management tricks and authentication token handling. Default implementations provided for authorization checking, session management and authentication token handling should make up for most basic cases.

Salient Features -

  • Extends SillyProxy inheriting all of its TLS sophistication and SNI goodness.
  • Allows to specify pluggable authentication scheme for each route that it terminates by implementing the Authenticator interface. Authenticators need to be registered with the authenticators map. How-to in the usage section to follow
  • One can also customize the way SniProxy checks incoming requests for authorization by implementing the authChecker interface. The default implementation defaultAuthChecker should meet most requirements of authorization and session management.
  • One can also customize their authTokens by implementing the authToken interface. Again, the default implementation defaultAuthToken should meet most cases.
  • TokenSetter interface can be extended to customize the placement and type of auth token returned in the proxy's response. The tokentype is inferred for each route from the routemap.json file. The default implementation defaultTokenSetter covers the common use cases - Cookie, Header and Either.
  • The proxy also supports defining locally handled requests by setting their routes as [-1, "localHandlerRegistryName"], where -1 indicates that the route is locally handled while "localHandlerRegistryName" is the name under which a LocalHandler implementation of LocalHandler interface is registered with RegisterLocalHandler. How-to in the Usage section to follow.
  • SniProxy requires that the following paths at a minimum are locally handled - "/authorizationError/" and "/requestUnauthorized/". You may choose to register the default implementations DefaultAuthorizationErrorRedirectPathLocalHandler and DefaultAuthorizationFailedRedirectPathLocalHandler if they serve your purpose.

Usage

  • A full featured sniproxy implementation making use of Google's OpenIDC as an authentication provider is provided in the example directory. This implementation can be used to deploy a minimalistic zero-trust beyond-corp proxy for authenticating remote users using Google OpenIDC to allow access to corporate assets without any VPN.

  • Create an authenticator that satisfies the Authenticator interface and register it with the pool of authenticators available for SniProxy.

  import "github.com/ChandraNarreddy/sniproxy"

  type myAuthenticator struct {
  }
  func (c *myAuthenticator) Authenticate(r *http.Request,
  	w http.ResponseWriter) (AuthenticatedPrincipal, ResponseFulfilledFlag, error) {

    //do what needs to be done.

    return "someUserPrincipalID", false, nil
  }
  type myPassThroughAuthenticator struct {
  }
  func (c *myPassThroughAuthenticator) Authenticate(r *http.Request,
  	w http.ResponseWriter) (AuthenticatedPrincipal, ResponseFulfilledFlag, error) {

    //do what needs to be done.

    return "AnonymousUser", false, nil
  }

  //Now register all the authenticators
  sniproxy.RegisterAuthenticator("myAuthenticatorAlias", &myAuthenticator{})
  sniproxy.RegisterAuthenticator("myPassthroughAuthenticatorAlias", &myAuthenticator{})
  • Register localhandlers for paths "/authorizationError/" and "/requestUnauthorized/" on each served host or override these paths for convenient global endpoints by overriding the paths as -
sniproxy.AuthorizationErrorRedirectPath = "https://proxyhostname/authorizationError"
sniproxy.AuthorizationFailedRedirectPath = "https://proxyhostname/requestUnauthorized"

These handlers should implement the LocalHandler interface.

  type myAuthorizationErrorLocalHandler struct {
  }
  func (c *myAuthorizationErrorLocalHandler) Handle(w http.ResponseWriter,
  	r *http.Request) {

  	//do what needs to be done with the request and respond back

  }

  type myAuthorizationFailureLocalHandler struct
  }
  func (c *myAuthorizationFailureLocalHandler) Handle(w http.ResponseWriter,
  	r *http.Request) {

  	//do what needs to be done with the request and respond back

  }

  //Now register the localhandlers with aliases
  sniproxy.RegisterLocalHandler("myAuthorizationErrorLocalHandlerAlias",
    &myAuthorizationErrorLocalHandler{})
  sniproxy.RegisterLocalHandler("myAuthorizationFailureLocalHandlerAlias",
    &myAuthorizationFailedRedirectPathLocalHandler{})
  • Alternatively, one can make use of the default local handler implementations -
sniproxy.RegisterLocalHandler("myAuthorizationErrorLocalHandlerAlias",
  &DefaultAuthorizationErrorRedirectPathLocalHandler{})
sniproxy.RegisterLocalHandler("myAuthorizationFailureLocalHandlerAlias",
  &DefaultAuthorizationFailedRedirectPathLocalHandler{})
  • Create an AuthToken implementation or use the provided one -
  defaultAuthToken := sniproxy.NewDefaultAuthToken("", nil)
  • Create an AuthChecker implementation or use the provided one -
  authChecker := sniproxy.NewDefaultAuthChecker(defaultAuthToken)
  • The defaultAuthChecker uses the defaultTokenSetter implementation. One can use a custom TokenSettter implementation though.
  • Next, we need to define routes. Routes are defined as JSON arrays and are composed of 'Host' to RoutePaths combinations. The 'Host' corresponds to the 'Host' header value of an incoming request. Please note that SniProxy cannot override an inbound request's method when it proxies a request. The Method and Path attributes act as filters to capture inbound requests. SniProxy uses httprouter under the hood and requires the path element to be defined using HTTPRouter's syntax.
  "Host":"www.mymainhostname",
  "MethodPathMaps": [
    {
      "Method":
      "Path" :
      "Route" :
      "AuthenticatorScheme":
      "TokenType" :
      "MaxRequestBodyBytes":
    }
  ]
Incoming Path

The Route attribute needs an array composed of a combination of strings and numbers in the exact sequence that makes up the proxy path for inbound request. The numbers (indexed from 0) correspond to respective parameter values that SniProxy extracts based on the Path that you defined for the route. SniProxy does a plain concatenation in order as defined in the sequence and constructs the proxy path it needs to follow. Please note that SniProxy does a URL escape over parameters it extracts from the incoming request before composing the proxy path. String values defined in the Route attribute are not escaped.

  "Path"  : "/blindforwarder/:scheme/:hostname/*end",
  "Route" : [ 0, "://", 1, "/", 2 ],

The above configuration instructs SniProxy to forward a requests for /blindforwarder/http/www.myhostname.com/getweather?forCity=mayhem to http://www.myhostname.com/getweather?forCity=mayhem

Incoming Query Params

Any incoming Query parameters are added as they are at the end of the path constructed.

AuthenticatorScheme

AuthenticatorScheme for each MethodPathMap needs to be defined and should have been registered in the registry.

  "AuthenticatorScheme": "myAuthenticator",
TokenType

TokenTypes that a particular request needs verified for. One can implement the TokenSetter interface to support custom TokenTypes.

"TokenType": "Cookie"
MaxRequestBodyBytes (Optional config)

MaxRequestBodyBytes is the maximum number of bytes the proxy should limit to reading from the incoming request body. This is an optional parameter, if its not specified, the proxy by default reads entire request body and forwards it onward. If a MaxRequestBodyBytes value is specified for a route and an incoming request exceeds the configured body length, the proxy throws a bad request error in response. Such requests are not even forwarded onward. This is primarily to be used if you are worried about Denial of Service or memory exhaustion and related QoS failures.

LocalHandlers

Any localhandlers that SniProxy needs to take note of should go in as routes in this pattern [-1, "localHandlerRegistryName"]. You have the flexibility to register the local handlers at the proxy level or at each individual host level by flipping the DefaultAuthorizationErrorRedirectPathLocalHandler and DefaultAuthorizationFailedRedirectPathLocalHandler values. By default, SniProxy defines them as relative paths for each host but it is easy to override them to global proxy level endpoints.

  "Route" : [-1, "myAuthorizationErrorLocalHandlerAlias"]

Create the necessary proxy configuration and save it, say "sniproxy_routes.json".

  {	"Routes":[
                {
                  "Host":"www.mymainhostname",
                  "MethodPathMaps": [
                                      {
                                        "Method": "GET",
                                        "Path"  : "/authorizationError/",
                                        "Route" : [-1, "myAuthorizationErrorLocalHandlerAlias"],
                                        "AuthenticatorScheme": "myPassThroughAuthenticator",
                                        "TokenType": "Cookie"
                                      },
                                      {
                                        "Method": "GET",
                                        "Path"  : "/requestUnauthorized/",
                                        "Route" : [-1, "myAuthorizationFailureLocalHandlerAlias"],
                                        "AuthenticatorScheme": "myPassThroughAuthenticator",
                                        "TokenType": "Either"
                                      },
                                      {
                                        "Method": "GET",
                                        "Path"  : "/wild/:domain/*end",
                                        "Route" : [ "https://www.",0,".com/", 1 ],
                                        "AuthenticatorScheme": "myAuthenticatorAlias",
                                        "TokenType": "Either"
                                      },
                                      {
                                        "Method": "POST",
                                        "Path"  : "/fileupload/1MBLimit",
                                        "Route" : [ "https://www.myinternalfileserver.com/"],
                                        "AuthenticatorScheme": "myAuthenticatorAlias",
                                        "TokenType": "Either",
                                        "MaxRequestBodyBytes": 1048576
                                      }
                                    ]
                },
                {
                  "Host":"www.myanotherhostname.com",
                  "MethodPathMaps": [
                                      {
                                        "Method": "GET",
                                        "Path"  : "/authorizationError/",
                                        "Route" : [-1, "myAuthorizationErrorLocalHandlerAlias"],
                                        "AuthenticatorScheme": "myPassThroughAuthenticator",
                                        "TokenType": "Cookie"
                                      },
                                      {
                                        "Method": "GET",
                                        "Path"  : "/requestUnauthorized/",
                                        "Route" : [-1, "myAuthorizationFailureLocalHandlerAlias"],
                                        "AuthenticatorScheme": "myPassThroughAuthenticator",
                                        "TokenType": "Either"
                                      },
                                      {
                                        "Method": "GET",
                                        "Path"  : "/blindforwarder/:scheme/:hostname/*end",
                                        "Route" : [ 0, "://", 1, "/", 2 ],
                                        "AuthenticatorScheme": "myAuthenticatorAlias",
                                        "TokenType": "Either"
                                      },
                                      {
                                        "Method": "GET",
                                        "Path"  : "/google/*query",
                                        "Route" : ["https://www.google.com/search?q=", 0],
                                        "AuthenticatorScheme": "myAuthenticatorAlias",
                                        "TokenType": "Cookie"
                                      }
                                    ]
                  }
                ]
    }
  • Generate the keystore for all the hosts that the proxy needs to terminate TLS connections.
  import "github.com/ChandraNarreddy/sniproxy/utility"

  var (

    //location of ecdsa certificate file for the main host
  	ecdsa_cert             = "ECDSA.cert"

    //location of ecdsa key file for the main host
  	ecdsa_key             = "ECDSA.key"

    //alias for the default certificate
  	alias_default         = "default"

    //hostname of the main host
    main_hostname = "www.mymainhostname.com"

    //location of certificate file for another host
  	rsa_cert               = "RSA.cert"

    //location of key file for another host
  	rsa_key               = "RSA.key"

    //hostname of another host
    anotherhostname = "www.myanotherhostname.com"

    ....//more as required//...

    //location where to locate/store the keystore file
  	keystore              = "keystore"

    //password to protect/open the keystore file
    keystorePassword = "ChangeMe"
  )

  //generate the keystore for the default hostname
  utility.GenerateKeyStore(&keystore, &alias_default, &ecdsa_cert, &ecdsa_key,
    &keystorePassword)
  utility.GenerateKeyStore(&keystore, &main_hostname, &ecdsa_cert, &ecdsa_key,
      &keystorePassword)

  //generate the keystore for additional hosts
  utility.GenerateKeyStore(&keystore, &anotherhostname, &rsa_cert, &rsa_key,
    &keystorePassword)

  //do the above for each additional host

Alternatively, you can make use of the CLI by building siillyproxy for your platform and generate the keystore like so -

./sillyProxy -keystore myKeyStore.ks -pemCert certificatteFile -pemKey pvtKeyFile -keypass changeme -hostname myExternalDomainName KeyStore

More here

  • Now invoke sniproxy -
  //specify the TLS version the proxy should run against, recommended - 3
  tlsVersion := uint(3)

  //specify the bind address for the proxy
  sniproxy_address := "0.0.0.0:443"

  //path to the routes file created above
  routefile_path := "sniproxy_routes.json"

  sniproxy, err := sniproxy.SniProxy(&keystore, &keystorePassword,
    &tlsVersion, &sniproxy_address, &routefile_path, authChecker)
  if err != nil {
    log.Fatalf("\nSetup fail: failed to fire sniproxy : %s", err)
  }
  sniproxy.ListenAndServeTLS("", "")

Contributing

Please submit issues for suggestions. Pull requests are welcome too.

Author

Chandrakanth Narreddy

License

MIT License

Acknowledgements

Documentation

Index

Constants

View Source
const (
	//DefaultAuthTokenName is SniProxyAuth
	DefaultAuthTokenName = "SniProxyAuth"
	//DefaultAuthTokenEncryptionKeySize is 32
	DefaultAuthTokenEncryptionKeySize = 32
	//DefaultAuthTokenExpirationDurationInHours is 12
	DefaultAuthTokenExpirationDurationInHours = 12
)
View Source
const (
	//COOKIE is used when authtoken should be handled as a http Cookie.
	COOKIE defaultAuthTokenType = iota
	//HEADER is used when authtoken should be handled in a http header (mobile apps)
	HEADER
	//EITHER choose this when you are not sure where the authtoken needs to be handled.
	// It will put the authtoken both in the cookie and as a header in the response and
	// also checks both places to find the authtoken in subsequent requests
	EITHER
)
View Source
const (
	ECDSA = 1
	RSA   = 2
)

ECDSA, RSA and DSA declared as enums

Variables

View Source
var (
	//AuthorizationErrorRedirectPath is the path where all requests are redirected
	// that return authChecker errors
	AuthorizationErrorRedirectPath = "/authorizationError/"
	//AuthorizationFailedRedirectPath is the path where all requests  are redirected
	// when authChecker has returned that the request was unauthorized
	AuthorizationFailedRedirectPath = "/requestUnauthorized/"
)
View Source
var (
	//DefaultAuthCheckerErr is returned when an unknown error occurs during authChecker run
	DefaultAuthCheckerErr = errors.New("Unknown Error occurred in DefaultAuthChecker")
	//DefaultAuthCheckerTokenMakerErr is returned when an error occurs during tokenMaker run
	DefaultAuthCheckerTokenMakerErr = errors.New("Error occurred while baking a token")
	//DefaultAuthCheckerTokenValidationErr is returned when an error occurs during token validation
	DefaultAuthCheckerTokenValidationErr = errors.New("Error occured while validating the token")
	//DefaultAuthCheckerTokenExpiredErr is returned when an authToken has expired its lifetime
	DefaultAuthCheckerTokenExpiredErr = errors.New("AuthToken has expired")
)
View Source
var (
	//ECDSAdefaultExists is a boolean that represents whether a ECDSA cert for the
	//default alias exists or not
	ECDSAdefaultExists = false
	//ECDSAdefault is used to hold ECDSA cert for default alias. Certs of default
	//alias are optimized to be grabbed this way instead of being part of certMap
	ECDSAdefault = &tls.Certificate{}

	//RSAdefaultExists is a boolean that represents whether a RSA cert for the
	//default alias exists or not
	RSAdefaultExists = false
	//RSAdefault is used to hold RSA cert for default alias. Certs of default
	//alias are optimized to be grabbed this way instead of being part of certMap
	RSAdefault = &tls.Certificate{}
)

declaring pointers to point at default cert to optimize for seeking default

View Source
var (
	//CiphersECDSA lists cipherSuite (as per http://www.iana.org/assignments/tls-parameters/tls-parameters.xml)
	//that allow for ECDSA signature based server authentication in TLS handshake
	CiphersECDSA = []uint16{
		0xC007, 0xC009, 0xC00A, 0xC023,
		0xC02B, 0xC02C, 0xCCA9, 0xC02D,
		0xC02E, 0xC024, 0xC025, 0xC026,
		0xC008}

	//CiphersRSA lists cipherSuite (as per http://www.iana.org/assignments/tls-parameters/tls-parameters.xml)
	//that allow for RSA signature based server authentication in TLS handshake
	CiphersRSA = []uint16{
		0xc011, 0xc012, 0xc013, 0xc014,
		0xc02f, 0xc030, 0xC027, 0x009C,
		0x009D, 0x0035, 0x003C, 0xCCA8,
		0x002F, 0x000A, 0x0005, 0x003C,
		0xC027, 0xC028}
)
View Source
var HopByHopHeaders = map[string]struct{}{
	"Connection":          {},
	"Keep-Alive":          {},
	"Proxy-Authenticate":  {},
	"Proxy-Authorization": {},
	"TE":                  {},
	"Trailer":             {},
	"Transfer-Encoding":   {},
	"Upgrade":             {},
}

Functions

func NewDefaultAuthChecker

func NewDefaultAuthChecker(token AuthToken) *defaultAuthChecker

NewDefaultAuthChecker function returns a new instance of defaultAuthChecker

func NewDefaultAuthToken

func NewDefaultAuthToken(authTokenName string, secret []byte) *defaultAuthToken

NewDefaultAuthToken generates a defaultAuthToken. Empty value for authTokenName will default its value to the DefaultAuthTokenName whereas nil value for secret results in choosing a random and ephemeral secret that remains active only for as long as the memory lives

func RegisterAuthenticator

func RegisterAuthenticator(name string, a Authenticator)

RegisterAuthenticator registers a new authenticator to the pool of authenticators available. Further, the main function of any consuming app should also declare a (non-referencing) import on the implementor package

func RegisterLocalHandler

func RegisterLocalHandler(name string, a LocalHandler)

RegisterLocalHandler is a function for any new LocalHandler to be registered and be made available in the localHandlers map. Further, the main function of any consuming app should also declare a (non-referencing) import on the implementor package

func SniProxy

func SniProxy(keyStoreFile *string, keyStorePass *string,
	minTLSVer *uint, bindAddr *string, routeMapFilePath *string,
	authChecker AuthChecker) (*http.Server, error)

SniProxy sets up certMap, proxyMap from keystore, routesInfo, authChecker and fires up

Types

type AuthChecker

type AuthChecker interface {
	// CheckAuth function takes http.Request, http.ResponseWriter and the
	// authenticationScheme
	// Returns whether the request is authorized or  not.
	// If the response had already been fulfilled in doing so,
	// the AuthChecker should return the responseFulfilled flag as True.
	CheckAuth(req *http.Request, rw http.ResponseWriter, authScheme string,
		tokenType string) (authorized bool, tokenSetter TokenSetter,
		responseFulfilledFlag bool, checkAuthError error)
}

AuthChecker is an interface that implements the CheckAuth function. A default implementation is provided

type AuthToken

type AuthToken interface {
	//Validate() function takes a token and the authScheme string arguments,
	// and returns whether the token is valid as a boolean, the AuthenticatedPrincipal
	// as a string and any error generated.
	Validate(encodedToken string, authScheme string) (validated bool, principal string, err error)
	//GetTokenName function should return the tokenName for the implementation of AuthToken
	GetTokenName() (tokenName string)
	//TokenMaker function takes the request, principal (userid) as a string, an expiry
	// parameter as time, authScheme as a string, an AuthTokenType implementation and returns
	//  a token in the form of a string and any error generated.
	TokenMaker(r *http.Request, principal string, expiry time.Time,
		authScheme string, tokenType AuthTokenType) (token string, err error)
}

AuthToken is a generic interface for implementations to satisfy as a stand-in for it.

type AuthTokenType

type AuthTokenType interface {
	//Implementations of AuthTokenType should expose a String() function
	String() string
}

AuthTokenType is a generic interface for implementations to satisfy as a stand-in for it.

type AuthenticatedPrincipal

type AuthenticatedPrincipal string

AuthenticatedPrincipal is the principal ID of the authenticated caller

type Authenticator

type Authenticator interface {
	//Authenticate and return the principal's identity, fulfill the request in the process if need be.
	//If the user could not be authenticated/authentication fails, set the authenticatedPrincipal to empty
	//If request is fulfilled while authenticating, set the ResponseFulfilledFlag as true else set as false.
	Authenticate(r *http.Request, w http.ResponseWriter) (AuthenticatedPrincipal, ResponseFulfilledFlag, error)
}

Authenticator is a generic interface for implementations to satisfy as a stand-in for it.

type DefaultAuthorizationErrorRedirectPathLocalHandler

type DefaultAuthorizationErrorRedirectPathLocalHandler struct {
}

DefaultAuthorizationErrorRedirectPathLocalHandler to handle authorization errors

func (*DefaultAuthorizationErrorRedirectPathLocalHandler) Handle

Handle handles authorization errors by returning a forbidden status

type DefaultAuthorizationFailedRedirectPathLocalHandler

type DefaultAuthorizationFailedRedirectPathLocalHandler struct {
}

DefaultAuthorizationFailedRedirectPathLocalHandler to handle authorization failures

func (*DefaultAuthorizationFailedRedirectPathLocalHandler) Handle

Handle handles authorization failures and returns Unauthorized status

type HostMap

type HostMap struct {
	Host           string          `json:"Host"`
	MethodPathMaps []MethodPathMap `json:"MethodPathMaps"`
}

HostMap lists the MethodPathMaps to each Host

type LocalHandler

type LocalHandler interface {
	//Handle handles the request and responds back on the writer
	Handle(w http.ResponseWriter, r *http.Request)
}

LocalHandler is a generic interface for implementations to satisfy as a stand-in for it.

type MethodPathMap

type MethodPathMap struct {
	Method              string        `json:"Method"`
	Path                string        `json:"Path"`
	Route               []interface{} `json:"Route"`
	AuthenticatorScheme string        `json:"AuthenticatorScheme"`
	TokenType           string        `json:"TokenType"`
	MaxRequestBodyBytes *int64        `json:"MaxRequestBodyBytes,omitempty"`
}

MethodPathMap maps each inbound method+path combination to backend route

type ResponseFulfilledFlag

type ResponseFulfilledFlag bool

ResponseFulfilledFlag is used to indicate to the callers if a request's response has been fulfilled during the function's run

type RouteMap

type RouteMap struct {
	Routes []HostMap `json:"Routes"`
}

RouteMap is a collection of HostMap called Routes

type TokenSetter

type TokenSetter interface {
	//SetToken takes the responsewriter rw and a tokenType string that indicates
	//placement of the token in the response (cookie, header, etc) inferred from
	//routemap json.
	SetToken(rw http.ResponseWriter, r *http.Request, tokenType string)
}

TokenSetter is a generic interface for implementations to satisfy as a stand-in for it.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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