graphql-engine/server/src-lib/Network/HTTP/Client/DynamicTlsPermissions.hs
Puru Gupta fcef6e5cb2 server: http ip blocklist (closes #2449)
## Description

This PR is in reference to #2449 (support IP blacklisting for multitenant)

*RFC Update: Add support for IPv6 blocking*

### Solution and Design

Using [http-client-restricted](https://hackage.haskell.org/package/http-client-restricted) package, we're creating the HTTP manager with restricting capabilities. The IPs can be supplied from the CLI arguments as `--ipv4BlocklistCidrs cidr1, cidr2...` or `--disableDefaultIPv4Blocklist` for a default IP list. The new manager will block all requests to the provided CIDRs.

We are extracting the error message string to show the end-user that given IP is blocked from being set as a webhook. There are 2 ways to extract the error message "connection to IP address is blocked". Given below are the responses from event trigger to a blocked IP for these implementations:
- 6d74fde316f61e246c861befcca5059d33972fa7 - We return the error message string as a HTTPErr(HOther) from `Hasura/Eventing/HTTP.hs`.
```
{
    "data": {
        "message": "blocked connection to private IP address "
    },
    "version": "2",
    "type": "client_error"
}
```

- 88e17456345cbb449a5ecd4877c84c9f319dbc25 - We case match on HTTPExceptionContent for InternaException in `Hasura/HTTP.hs` and extract the error message string from it. (this is implemented as it handles all the cases where pro engine makes webhook requests)
```
{
  "data": {
    "message": {
      "type": "http_exception",
      "message": "blocked connection to private IP address ",
      "request": {
        "secure": false,
        "path": "/webhook",
        "responseTimeout": "ResponseTimeoutMicro 60000000",
        "queryString": "",
        "method": "POST",
        "requestHeaders": {
          "Content-Type": "application/json",
          "X-B3-ParentSpanId": "5ae6573edb2a6b36",
          "X-B3-TraceId": "29ea7bd6de6ebb8f",
          "X-B3-SpanId": "303137d9f1d4f341",
          "User-Agent": "hasura-graphql-engine/cerebushttp-ip-blacklist-a793a0e41-dirty"
        },
        "host": "139.59.90.109",
        "port": 8000
      }
    }
  },
  "version": "2",
  "type": "client_error"
}
```

### Steps to test and verify
The restricted IPs can be used as webhooks in event triggers, and hasura will return an error message in reponse.

### Limitations, known bugs & workarounds
- The `http-client-restricted` has a needlessly complex interface, and puts effort into implementing proxy support which we don't want, so we've inlined a stripped down version.
- Performance constraint: As the blocking is checked for each request, if a long list of blocked CIDRs is supplied, iterating through all of them is not what we would prefer. Using trie is suggested to overcome this. (Added to RFC)
- Calls to Lux endpoints are inconsistent: We use either the http manager from the ProServeCtx which is unrestricted, or the http manager from the ServeCtx which is restricted (the latter through the instances for MonadMetadataApiAuthorization and UserAuthentication). (The failure scenario here would be: cloud sets PRO_ENDPOINT to something that resolves to an internal address, and then restricted requests to those endpoints fail, causing auth to fail on user requests. This is about HTTP requests to lux auth endpoints.)

## Changelog

-  `CHANGELOG.md` is updated with user-facing content relevant to this PR.

## Affected components

-  Server
-  Tests

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3186
Co-authored-by: Robert <132113+robx@users.noreply.github.com>
GitOrigin-RevId: 5bd2de2d028bc416b02c99e996c7bebce56fb1e7
2022-02-25 13:30:57 +00:00

83 lines
3.8 KiB
Haskell

module Network.HTTP.Client.DynamicTlsPermissions
( dynamicTlsSettings,
)
where
import Control.Exception.Safe (Exception, Typeable, impureThrow)
import Data.ByteString.Char8 qualified as BC
import Data.Default.Class qualified as HTTP
import Data.X509 qualified as HTTP
import Data.X509.CertificateStore qualified as HTTP
import Data.X509.Validation qualified as HTTP
import GHC.Exception (Exception (displayException))
import Hasura.Prelude
import Hasura.RQL.Types.Network (TlsAllow (TlsAllow), TlsPermission (SelfSigned))
import Network.Connection qualified as HTTP
import Network.TLS qualified as HTTP
import Network.TLS.Extra qualified as TLS
import System.X509 qualified as HTTP
newtype TlsServiceDefinitionError = TlsServiceDefinitionError
{ tlsServiceDefinitionError :: String
}
deriving (Show, Typeable)
instance Exception TlsServiceDefinitionError where
displayException (TlsServiceDefinitionError msg) = "TlsServiceDefinitionError: " <> show msg
errorE :: String -> c
errorE = impureThrow . TlsServiceDefinitionError
dynamicTlsSettings :: IO [TlsAllow] -> IO HTTP.TLSSettings
dynamicTlsSettings currentAllow = do
systemStore <- HTTP.getSystemCertificateStore
return (tlsSettingsComplex systemStore)
where
tlsSettingsComplex :: HTTP.CertificateStore -> HTTP.TLSSettings
tlsSettingsComplex systemStore = HTTP.TLSSettings (clientParams systemStore)
clientParams :: HTTP.CertificateStore -> HTTP.ClientParams
clientParams systemStore =
(HTTP.defaultParamsClient hostName serviceIdBlob)
{ HTTP.clientSupported = HTTP.def {HTTP.supportedCiphers = TLS.ciphersuite_default}, -- supportedCiphers :: [Cipher] Supported cipher methods. The default is empty, specify a suitable cipher list. ciphersuite_default is often a good choice. Default: [] -- https://hackage.haskell.org/package/tls-1.5.5/docs/Network-TLS.html#t:Cipher
HTTP.clientShared = HTTP.def {HTTP.sharedCAStore = systemStore},
HTTP.clientHooks =
HTTP.def
{ HTTP.onServerCertificate = certValidation
}
}
certValidation :: HTTP.CertificateStore -> HTTP.ValidationCache -> HTTP.ServiceID -> HTTP.CertificateChain -> IO [HTTP.FailedReason]
certValidation certStore validationCache sid chain = do
res <- HTTP.onServerCertificate HTTP.def certStore validationCache sid chain
allowList <- currentAllow
if any (allowed sid res) allowList
then pure []
else pure res
-- These always seem to be overwritten when a connection is established
-- Should leave as errors in this case in order to validate this assumption.
-- TODO: Is there any way to define this in terms of a pure exception?
hostName = errorE "hostname in HTTP client defaultParamsClient accessed - this should never happen"
serviceIdBlob = errorE "serviceIdBlob in HTTP client defaultParamsClient accessed - this should never happen"
-- Checks that:
allowed :: (String, BC.ByteString) -> [HTTP.FailedReason] -> TlsAllow -> Bool
allowed (sHost, sPort) res (TlsAllow aHost aPort aPermit) =
(sHost == aHost)
&& (BC.unpack sPort ==? aPort)
&& all (\x -> any (($ x) . permitted) (fromMaybe [SelfSigned] aPermit)) res
-- TODO: Could clean up this check some more.
-- Comments on failure reasons taken from https://hackage.haskell.org/package/x509-validation-1.4.7/docs/src/Data-X509-Validation.html
-- The permitted function takes high-level concerns and translates then into certain permitted errors
permitted SelfSigned HTTP.SelfSigned = True -- Certificate is self signed
permitted SelfSigned (HTTP.NameMismatch _) = True -- Connection name and certificate do not match
permitted SelfSigned HTTP.LeafNotV3 = True -- Only authorized an X509.V3 certificate as leaf certificate.
permitted SelfSigned _ = False
_ ==? Nothing = True
a ==? Just a' = a == a'