{-# LANGUAGE OverloadedStrings #-} module Kubernetes.Client.Config where import qualified Kubernetes.OpenAPI.Core as K import qualified Kubernetes.OpenAPI.Model as K import Control.Exception.Safe (Exception, MonadThrow, throwM) import Control.Monad.IO.Class (MonadIO, liftIO) import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as LazyB import Data.Default.Class (def) import Data.Either (rights) import Data.Monoid ((<>)) import Data.PEM (pemContent, pemParseBS) import qualified Data.Text as T import qualified Data.Text.Encoding as T import qualified Data.Text.IO as T import Data.Typeable (Typeable) import Data.X509 (SignedCertificate, decodeSignedCertificate) import qualified Data.X509 as X509 import Data.X509.CertificateStore (CertificateStore, makeCertificateStore) import qualified Data.X509.Validation as X509 import Lens.Micro (Lens', lens, set) import Network.Connection (TLSSettings (..)) import qualified Network.HTTP.Client as NH import Network.HTTP.Client.TLS (mkManagerSettings) import Network.TLS (Credential, defaultParamsClient) import qualified Network.TLS as TLS import qualified Network.TLS.Extra as TLS import System.Environment (getEnv) import System.X509 (getSystemCertificateStore) -- |Sets the master URI in the 'K.KubernetesClientConfig'. setMasterURI :: T.Text -- ^ Master URI -> K.KubernetesClientConfig -> K.KubernetesClientConfig setMasterURI server kcfg = kcfg { K.configHost = (LazyB.fromStrict . T.encodeUtf8) server } -- |Disables the client-side auth methods validation. This is necessary if you are using client cert authentication. disableValidateAuthMethods :: K.KubernetesClientConfig -> K.KubernetesClientConfig disableValidateAuthMethods kcfg = kcfg { K.configValidateAuthMethods = False } -- |Configures the 'K.KubernetesClientConfig' to use token authentication. setTokenAuth :: T.Text -- ^Authentication token -> K.KubernetesClientConfig -> K.KubernetesClientConfig setTokenAuth token kcfg = kcfg { K.configAuthMethods = [K.AnyAuthMethod (K.AuthApiKeyBearerToken $ "Bearer " <> token)] } -- |Creates a 'NH.Manager' that can handle TLS. newManager :: TLS.ClientParams -> IO NH.Manager newManager cp = NH.newManager (mkManagerSettings (TLSSettings cp) Nothing) -- |Default TLS settings using the system CA store. defaultTLSClientParams :: IO TLS.ClientParams defaultTLSClientParams = do let defParams = defaultParamsClient "" "" systemCAStore <- getSystemCertificateStore return defParams { TLS.clientSupported = def { TLS.supportedCiphers = TLS.ciphersuite_strong } , TLS.clientShared = (TLS.clientShared defParams) { TLS.sharedCAStore = systemCAStore } } clientHooksL :: Lens' TLS.ClientParams TLS.ClientHooks clientHooksL = lens TLS.clientHooks (\cp ch -> cp { TLS.clientHooks = ch }) onServerCertificateL :: Lens' TLS.ClientParams (CertificateStore -> TLS.ValidationCache -> X509.ServiceID -> X509.CertificateChain -> IO [X509.FailedReason]) onServerCertificateL = clientHooksL . lens TLS.onServerCertificate (\ch osc -> ch { TLS.onServerCertificate = osc }) -- |Don't check whether the cert presented by the server matches the name of the server you are connecting to. -- This is necessary if you specify the server host by its IP address. disableServerNameValidation :: TLS.ClientParams -> TLS.ClientParams disableServerNameValidation = set onServerCertificateL (X509.validate X509.HashSHA256 def (def { X509.checkFQHN = False })) -- |Insecure mode. The client will not validate the server cert at all. disableServerCertValidation :: TLS.ClientParams -> TLS.ClientParams disableServerCertValidation = set onServerCertificateL (\_ _ _ _ -> return []) -- |Use a custom CA store. setCAStore :: [SignedCertificate] -> TLS.ClientParams -> TLS.ClientParams setCAStore certs cp = cp { TLS.clientShared = (TLS.clientShared cp) { TLS.sharedCAStore = (makeCertificateStore certs) } } onCertificateRequestL :: Lens' TLS.ClientParams (([TLS.CertificateType], Maybe [TLS.HashAndSignatureAlgorithm], [X509.DistinguishedName]) -> IO (Maybe (X509.CertificateChain, TLS.PrivKey))) onCertificateRequestL = clientHooksL . lens TLS.onCertificateRequest (\ch ocr -> ch { TLS.onCertificateRequest = ocr }) -- |Use a client cert for authentication. setClientCert :: Credential -> TLS.ClientParams -> TLS.ClientParams setClientCert cred = set onCertificateRequestL (\_ -> return $ Just cred) -- |Parses a PEM-encoded @ByteString@ into a list of certificates. parsePEMCerts :: B.ByteString -> Either String [SignedCertificate] parsePEMCerts b = do pems <- pemParseBS b return $ rights $ map (decodeSignedCertificate . pemContent) pems data ParsePEMCertsException = ParsePEMCertsException String deriving (Typeable, Show) instance Exception ParsePEMCertsException -- |Loads certificates from a PEM-encoded file. loadPEMCerts :: (MonadIO m, MonadThrow m) => FilePath -> m [SignedCertificate] loadPEMCerts p = do liftIO (B.readFile p) >>= either (throwM . ParsePEMCertsException) return . parsePEMCerts serviceAccountDir :: FilePath serviceAccountDir = "/var/run/secrets/kubernetes.io/serviceaccount" cluster :: (MonadIO m, MonadThrow m) => m (NH.Manager, K.KubernetesClientConfig) cluster = do caStore <- loadPEMCerts $ serviceAccountDir ++ "/ca.crt" defTlsParams <- liftIO defaultTLSClientParams mgr <- liftIO . newManager . setCAStore caStore $ disableServerNameValidation defTlsParams tok <- liftIO . T.readFile $ serviceAccountDir ++ "/token" host <- liftIO $ getEnv "KUBERNETES_SERVICE_HOST" port <- liftIO $ getEnv "KUBERNETES_SERVICE_PORT" config <- setTokenAuth tok . setMasterURI (T.pack $ "https://" ++ host ++ ":" ++ port) <$> liftIO K.newConfig return (mgr, config)