diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/aws-ec2-knownhosts.cabal b/aws-ec2-knownhosts.cabal
new file mode 100644
--- /dev/null
+++ b/aws-ec2-knownhosts.cabal
@@ -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
diff --git a/exec/aws-ec2-keysync/Main.hs b/exec/aws-ec2-keysync/Main.hs
new file mode 100644
--- /dev/null
+++ b/exec/aws-ec2-keysync/Main.hs
@@ -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
diff --git a/exec/aws-ec2-knownhosts/Main.hs b/exec/aws-ec2-knownhosts/Main.hs
new file mode 100644
--- /dev/null
+++ b/exec/aws-ec2-knownhosts/Main.hs
@@ -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"
diff --git a/exec/aws-ec2-pubkeys/Main.hs b/exec/aws-ec2-pubkeys/Main.hs
new file mode 100644
--- /dev/null
+++ b/exec/aws-ec2-pubkeys/Main.hs
@@ -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
diff --git a/src/AWS/KnownHosts.hs b/src/AWS/KnownHosts.hs
new file mode 100644
--- /dev/null
+++ b/src/AWS/KnownHosts.hs
@@ -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 {}"
diff --git a/src/AWS/PubKeys.hs b/src/AWS/PubKeys.hs
new file mode 100644
--- /dev/null
+++ b/src/AWS/PubKeys.hs
@@ -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"
diff --git a/src/AWS/Types.hs b/src/AWS/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/AWS/Types.hs
@@ -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
+        ]
