aws-ec2-knownhosts (empty) → 0.1.0.0
raw patch · 8 files changed
+463/−0 lines, 8 filesdep +aesondep +asyncdep +attoparsec
Dependencies added: aeson, async, attoparsec, aws-ec2-knownhosts, base, bytestring, directory, filepath, io-streams, process, system-filepath, text, text-format, turtle
Files
- LICENSE +29/−0
- aws-ec2-knownhosts.cabal +76/−0
- exec/aws-ec2-keysync/Main.hs +85/−0
- exec/aws-ec2-knownhosts/Main.hs +35/−0
- exec/aws-ec2-pubkeys/Main.hs +30/−0
- src/AWS/KnownHosts.hs +56/−0
- src/AWS/PubKeys.hs +93/−0
- src/AWS/Types.hs +59/−0
+ LICENSE view
@@ -0,0 +1,29 @@+BSD 3-Clause License++Copyright (c) 2015-present, Bitnomial, Inc.+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+ list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+ this list of conditions and the following disclaimer in the documentation+ and/or other materials provided with the distribution.++* Neither the name of the copyright holder nor the names of its+ contributors may be used to endorse or promote products derived from+ this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ aws-ec2-knownhosts.cabal view
@@ -0,0 +1,76 @@+name: aws-ec2-knownhosts+version: 0.1.0.0+synopsis: Capture and manage AWS EC2 known_host pubkeys+description: Capture and manage AWS EC2 known_host pubkeys via the AWS API command line tool.+license: BSD3+license-file: LICENSE+author: Luke Hoersten+maintainer: luke@bitnomial.com, opensource@bitnomial.com+copyright: Bitnomial, Inc. (c) 2015+category: AWS, Network+build-type: Simple+cabal-version: >=1.10+homepage: http://github.com/bitnomial/aws-ec2-knownhosts+bug-reports: http://github.com/bitnomial/aws-ec2-knownhosts/issues+++library+ hs-source-dirs: src+ default-language: Haskell2010++ exposed-modules: AWS.KnownHosts+ , AWS.PubKeys+ , AWS.Types++ build-depends: base >= 4.7 && < 4.11+ , aeson >= 1.0 && < 1.3+ , async >= 2.0 && < 2.2+ , attoparsec >= 0.12 && < 0.14+ , bytestring >= 0.10 && < 0.11+ , directory >= 1.2 && < 1.4+ , filepath >= 1.4 && < 1.5+ , io-streams >= 1.3.4 && < 1.5+ , process >= 1.4 && < 1.7+ , text >= 1.1 && < 1.3+ , text-format >= 0.3 && < 0.4+++executable aws-ec2-pubkeys+ main-is: Main.hs+ hs-source-dirs: exec/aws-ec2-pubkeys/+ default-language: Haskell2010+ ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2 -rtsopts++ build-depends: base >= 4.7 && < 4.11+ , aws-ec2-knownhosts+ , io-streams >= 1.1 && < 1.5+ , system-filepath >= 0.4 && < 0.5+ , turtle >= 1.3 && < 1.5+++executable aws-ec2-knownhosts+ main-is: Main.hs+ hs-source-dirs: exec/aws-ec2-knownhosts/+ default-language: Haskell2010+ ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2 -rtsopts++ build-depends: base >= 4.7 && < 4.11+ , aeson >= 1.0 && < 1.3+ , aws-ec2-knownhosts+ , io-streams >= 1.1 && < 1.5+ , system-filepath >= 0.4 && < 0.5+ , turtle >= 1.3 && < 1.5+++executable aws-ec2-keysync+ main-is: Main.hs+ hs-source-dirs: exec/aws-ec2-keysync/+ default-language: Haskell2010+ ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2 -rtsopts++ build-depends: base >= 4.7 && < 4.11+ , aeson >= 1.0 && < 1.3+ , aws-ec2-knownhosts+ , io-streams >= 1.1 && < 1.5+ , system-filepath >= 0.4 && < 0.5+ , turtle >= 1.3 && < 1.5
+ exec/aws-ec2-keysync/Main.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Control.Monad ((<=<))+import Data.Aeson (Result (..), fromJSON, json')+import Data.Bool (bool)+import Data.Function (on)+import Data.List (foldl')+import Data.Monoid ((<>))+import Filesystem.Path.CurrentOS (encodeString, fromText)+import Prelude hiding (FilePath)+import qualified System.IO.Streams as Streams+import System.IO.Streams.Attoparsec (parseFromStream)+import Turtle (ExitCode (..), FilePath, Parser, Text, argPath, argText, exit, format,+ fp, isRegularFile, options, proc, rm, stat)++import AWS.PubKeys (writePubKeys)+import AWS.Types (Ec2Instance (..))+++main :: IO ()+main = options "Synchronize pubkeys from EC2 to AWS S3" args >>= processPubKeys+++args :: Parser (FilePath, FilePath, Text)+args = (,,)+ <$> argPath "local_pubkey_filename" "Local pubkey file to send to S3"+ <*> argPath "remote_pubkey_filename" "Destination pubkey file to send to S3"+ <*> argText "s3_bucket_name" "AWS S3 bucket name to store pubkeys"+++processPubKeys :: (FilePath, FilePath, Text) -> IO ()+processPubKeys (localPubKeyFile, remotePubKeyFile, bucketName) = do+ newPubKeys <- readPubKeys localPubKeyFile+ remotePubKeys <- getFromS3 remotePubKeyFile tempRemoteFile bucketName+ writePubKeys (encodeString mergedKeyFile) $ merge remotePubKeys newPubKeys+ copyToS3 mergedKeyFile remotePubKeyFile bucketName+ where+ tempRemoteFile :: FilePath+ tempRemoteFile = "/tmp/" <> fromText bucketName <> "-" <> remotePubKeyFile+ mergedKeyFile = "/tmp/merged-keys.json"+++readPubKeys :: FilePath -> IO [Ec2Instance]+readPubKeys pubKeyFile =+ Streams.withFileAsInput (encodeString pubKeyFile) (handleResult <=< parseFromStream pubKeyParser)+ where pubKeyParser = fmap fromJSON json'+++merge :: [Ec2Instance] -> [Ec2Instance] -> [Ec2Instance]+merge = foldl' replace+ where+ replace instancePubKeys newInstancePubKey =+ newInstancePubKey : filter (compareFqdn newInstancePubKey) instancePubKeys+++compareFqdn :: Ec2Instance -> Ec2Instance -> Bool+compareFqdn = (/=) `on` fqdn+++handleResult :: Result a -> IO a+handleResult (Success a) = return a+handleResult (Error s) = putStrLn ("Parse error: " <> s) >> exit (ExitFailure (-1))+++getFromS3 :: FilePath -> FilePath -> Text -> IO [Ec2Instance]+getFromS3 remoteFile tempRemoteFile bucketName = do+ _ <- proc "aws" [ "s3", "cp" , source, format fp tempRemoteFile ] ""+ isRegularFile <$> stat tempRemoteFile >>= bool (return []) (readPubKeys tempRemoteFile)+ where+ source = "s3://" <> bucketName <> "/" <> format fp remoteFile+++copyToS3 :: FilePath -> FilePath -> Text -> IO ()+copyToS3 localFile remoteFile bucketName =+ proc "aws" ["s3", "cp", format fp localFile, dest] "" >>= checkUpload+ where+ dest = "s3://" <> bucketName <> "/" <> format fp remoteFile+ checkUpload ExitSuccess = putStrLn "Upload successful"+ checkUpload _ = putStrLn "Failure uploading pubkey file. Check AWS credentials." >> exit (ExitFailure (-1))+++cleanup :: FilePath -> FilePath -> IO ()+cleanup tempRemoteFile mergedKeyFile = rm tempRemoteFile >> rm mergedKeyFile
+ exec/aws-ec2-knownhosts/Main.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Data.Aeson (Result (..), fromJSON, json)+import Data.Maybe (maybe)+import Filesystem.Path.CurrentOS (encodeString)+import Prelude hiding (FilePath)+import qualified System.IO.Streams as Streams+import System.IO.Streams.Attoparsec (parseFromStream)+import Turtle (FilePath, Parser, argPath, options)++import AWS.KnownHosts (updateKnownHosts)+import AWS.Types (Ec2Instance (..))+++main :: IO ()+main = options "Update known_hosts file with pubkeys from EC2" args >>= readKeys >>= maybe noKeys updateKnownHosts+++args :: Parser FilePath+args = argPath "pubkey_file" "Path to JSON pubkey file to update knownhosts"+++readKeys :: FilePath -> IO (Maybe [Ec2Instance])+readKeys = fmap (maybeResult . fromJSON) . flip Streams.withFileAsInput (parseFromStream json) . encodeString+++maybeResult :: Result a -> Maybe a+maybeResult (Success x) = Just x+maybeResult (Error _) = Nothing+++noKeys :: IO ()+noKeys = putStrLn "No keys were found in that pub_key_file"
+ exec/aws-ec2-pubkeys/Main.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Filesystem.Path.CurrentOS (encodeString)+import qualified System.IO.Streams as Streams+import System.IO.Streams.Attoparsec (parseFromStream)+import Turtle (Parser, argPath, options)++import AWS.PubKeys (getPubKeys, instanceParser, writePubKeys)+import AWS.Types (Ec2Instance (..))+++main :: IO ()+main = options "Get SSH pubkeys from EC2" args >>= processPubKeys+++args :: Parser (FilePath, FilePath)+args = (,)+ <$> (encodeString <$> argPath "instance_file" "File with information about EC2 hosts")+ <*> (encodeString <$> argPath "pubkey_file" "Path to JSON pubkey file to output")+++-- TODO Implement a function to poll until all pubkeys are available+processPubKeys :: (FilePath, FilePath) -> IO ()+processPubKeys (instanceFile, pubKeyFile) = writePubKeys pubKeyFile =<< getPubKeys =<< readInstances instanceFile+++readInstances :: FilePath -> IO [Ec2Instance]+readInstances = ($ parseFromStream instanceParser) . Streams.withFileAsInput
+ src/AWS/KnownHosts.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}++module AWS.KnownHosts+ ( updateKnownHosts+ ) where++import Data.ByteString.Char8 (ByteString)+import Data.Monoid ((<>))+import Data.Text.Encoding (encodeUtf8)+import Data.Text.Format (Only (..), format)+import qualified Data.Text.Lazy as LT+import GHC.IO.Handle (BufferMode (NoBuffering))+import Prelude hiding (filter, takeWhile)+import System.Directory (getHomeDirectory)+import System.Exit (ExitCode)+import System.FilePath.Posix ((</>))+import System.IO (IOMode (AppendMode))+import qualified System.IO.Streams as Streams+import System.IO.Streams.File (withFileAsOutputExt)+import System.IO.Streams.Process (runInteractiveCommand)+import System.Process (waitForProcess)++import AWS.Types (Ec2Instance (..), Key (..))+++updateKnownHosts :: [Ec2Instance] -> IO ()+updateKnownHosts ks = do+ homeDir <- getHomeDirectory+ withFileAsOutputExt (knownHosts homeDir) AppendMode NoBuffering $ updateKeys ks+ where+ knownHosts homeDir = homeDir </> ".ssh" </> "known_hosts"+++updateKeys :: [Ec2Instance] -> Streams.OutputStream ByteString -> IO ()+updateKeys ks out = do+ out' <- Streams.intersperse "\n" out+ mapM_ (updateKey out') ks+ Streams.write (Just "\n") out+++updateKey :: Streams.OutputStream ByteString -> Ec2Instance -> IO ()+updateKey out inst | Just k <- instancePubKey inst =+ removeKey inst `seq`+ Streams.write (Just $ encodeUtf8 (fqdn inst <> "," <> dns inst <> " " <> keyType k <> " " <> pubKey k)) out+updateKey _ _ = return ()+++removeKey :: Ec2Instance -> IO ExitCode+removeKey inst = do+ (_,_,_,pid1) <- runInteractiveCommand . LT.unpack . format removeKeyCommand . Only $ dns inst+ waitForProcess pid1+ (_,_,_,pid2) <- runInteractiveCommand . LT.unpack . format removeKeyCommand . Only $ fqdn inst+ waitForProcess pid2+ where+ removeKeyCommand = "ssh-keygen -R {}"
+ src/AWS/PubKeys.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}++module AWS.PubKeys+ ( getPubKeys+ , writePubKeys+ , instanceParser+ ) where++import Control.Applicative (Alternative, (*>), (<*), (<|>))+import Control.Concurrent.Async (mapConcurrently)+import Data.Aeson (decodeStrict', encode)+import qualified Data.Attoparsec.ByteString as W8+import Data.Attoparsec.ByteString.Char8 (Parser, endOfInput, endOfLine, isEndOfLine, isSpace, manyTill',+ skipSpace, string, (<?>))+import qualified Data.Attoparsec.ByteString.Char8 as C8+import Data.ByteString.Char8 (ByteString, filter)+import Data.ByteString.Lazy.Char8 (toStrict)+import Data.List (find)+import Data.Maybe (catMaybes, mapMaybe)+import Data.Text.Encoding (decodeUtf8)+import Data.Text.Format (format)+import qualified Data.Text.Lazy as LT+import Prelude hiding (filter, takeWhile)+import qualified System.IO.Streams as Streams+import System.IO.Streams.Attoparsec (parseFromStream)+import System.IO.Streams.Process (runInteractiveCommand)++import AWS.Types (Ec2Instance (..), Key (..))+++getPubKeys :: [Ec2Instance] -> IO [Ec2Instance]+getPubKeys = fmap catMaybes <$> mapConcurrently getPubKey+++getPubKey :: Ec2Instance -> IO (Maybe Ec2Instance)+getPubKey inst = do+ (_, stdout, _, _) <- runInteractiveCommand command+ keys' <- Streams.map (filter (/= '\r')) stdout >>= parseFromStream keyParser+ return $ (\k -> inst{instancePubKey=Just k}) <$> find ((keytype' ==) . keyType) keys'+ where+ keytype' = "ecdsa-sha2-nistp256"+ command = LT.unpack $ format consoleOutput (region inst, instanceId inst)+ consoleOutput = "aws ec2 get-console-output --region {} --output text --instance-id {}"+++writePubKeys :: FilePath -> [Ec2Instance] -> IO ()+writePubKeys file = Streams.withFileAsOutput file . Streams.write . Just . toStrict . encode+++instanceParser :: Parser [Ec2Instance]+instanceParser = mapMaybe decodeStrict' <$> (W8.takeTill isEndOfLine `W8.sepBy` endOfLine <* endOfInput) <?> "instances"+++keyParser :: Parser [Key]+keyParser = skipToKeys *> keys <?> "keyParser"+++skipToKeys :: Parser ()+skipToKeys = skipLine `skipTill` keysBegin <?> "skipToKeys"+ where+ skipTill :: Alternative f => f a -> f b -> f b+ skipTill s n = n <|> s *> skipTill s n+++skipLine :: Parser ()+skipLine = W8.skipWhile (not . isEndOfLine) <* endOfLine <?> "skip line"+++line :: ByteString -> Parser ()+line s = string s *> endOfLine <?> "exact line"+++keysBegin :: Parser ()+keysBegin = line "-----BEGIN SSH HOST KEY KEYS-----" <?> "keys begin"+++keysEnd :: Parser ()+keysEnd = line "-----END SSH HOST KEY KEYS-----" <?> "keys end"+++keys :: Parser [Key]+keys = manyTill' key keysEnd <?> "keys"+++key :: Parser Key+key = do+ keyType' <- decodeUtf8 <$> C8.takeTill isSpace+ skipSpace+ pubKey' <- decodeUtf8 <$> C8.takeTill isSpace+ skipLine+ return $ Key keyType' pubKey'+ <?> "key"
+ src/AWS/Types.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE OverloadedStrings #-}++module AWS.Types where++import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), object, (.:), (.:?), (.=))+import Data.Aeson.Types (typeMismatch)+import Data.Text (Text)+++data Key = Key+ { keyType :: Text+ , pubKey :: Text+ } deriving (Show, Eq)+++data Ec2Instance = Ec2Instance+ { fqdn :: Text+ , region :: Text+ , instanceId :: Text+ , dns :: Text+ , initKey :: Text+ , instancePubKey :: Maybe Key+ } deriving (Show, Eq)+++instance FromJSON Key where+ parseJSON (Object v) = Key+ <$> v .: "type"+ <*> v .: "pubkey"+ parseJSON invalid = typeMismatch "Key" invalid+++instance ToJSON Key where+ toJSON x = object+ [ "type" .= keyType x+ , "pubkey" .= pubKey x+ ]+++instance FromJSON Ec2Instance where+ parseJSON (Object v) = Ec2Instance+ <$> v .: "fqdn"+ <*> v .: "region"+ <*> v .: "instance_id"+ <*> v .: "dns"+ <*> v .: "init_key"+ <*> v .:? "pubkey"+ parseJSON invalid = typeMismatch "Ec2Instance" invalid+++instance ToJSON Ec2Instance where+ toJSON x = object+ [ "fqdn" .= fqdn x+ , "region" .= region x+ , "instance_id" .= instanceId x+ , "dns" .= dns x+ , "init_key" .= initKey x+ , "pubkey" .= instancePubKey x+ ]