diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2016 Bit Connor
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+# vault-tool-server
+
+Utility library for spawning a HashiCorp Vault process.
+
+This package also contains the tests for the related
+[vault-tool](../vault-tool/) package (as a standard cabal test suite)
+
+Running the test suite requires that the "vault" executable is in your `$PATH`,
+or alternatively you may set the `VAULT_EXE` environment variable.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/src/Network/VaultTool/VaultServerProcess.hs b/src/Network/VaultTool/VaultServerProcess.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/VaultTool/VaultServerProcess.hs
@@ -0,0 +1,227 @@
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Network.VaultTool.VaultServerProcess
+    ( VaultServerProcess
+    , launchVaultServerProcess
+    , shutdownVaultServerProcess
+    , withVaultServerProcess
+
+    , VaultBackendConfig
+    , withVaultConfigFile
+    , vaultConfigDefaultAddress
+    , vaultAddress
+
+    , readVaultBackendConfig
+    , readVaultUnsealKeys
+    ) where
+
+import Control.Concurrent (threadDelay)
+import Control.Concurrent.Async
+import Control.Exception (Exception, IOException, catches, Handler(Handler), bracket, bracketOnError, throwIO, try)
+import Control.Monad (forever)
+import Data.Aeson
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import Network.HTTP.Client (HttpException)
+import System.Exit (ExitCode)
+import System.FilePath ((</>))
+import System.IO (Handle, hClose)
+import System.IO.Temp
+import System.Process
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+
+import Network.VaultTool
+
+-- | The ""backend"" section of the Vault server configuration.
+--
+-- See <https://www.vaultproject.io/docs/config/index.html>
+--
+-- > {
+-- >   "consul": {
+-- >     "address": "127.0.0.1:8500",
+-- >     "path": "vault"
+-- >   }
+-- > }
+--
+-- > {
+-- >   "file": {
+-- >     "path": "vault-storage"
+-- >   }
+-- > }
+type VaultBackendConfig = Value
+
+data VaultConfig = VaultConfig
+    { _VaultConfig_Backend :: VaultBackendConfig
+    , _VaultConfig_ListenAddress :: Text
+    }
+    deriving (Show)
+
+instance ToJSON VaultConfig where
+    toJSON VaultConfig{..} = object
+        [ "backend" .= _VaultConfig_Backend
+        , "listener" .= object
+            [ "tcp" .= object
+                [ "tls_disable" .= T.pack "true"
+                , "address" .= _VaultConfig_ListenAddress
+                ]
+            ]
+        , "disable_mlock" .= True
+        ]
+
+vaultConfigDefaultAddress :: VaultBackendConfig -> VaultConfig
+vaultConfigDefaultAddress b =
+    VaultConfig
+        { _VaultConfig_Backend = b
+        , _VaultConfig_ListenAddress = defaultAddress
+        }
+    where
+    defaultAddress = "127.0.0.1:8200"
+
+-- | Get the address that can be used to connect to a running Vault server
+-- launched with the specified config.
+--
+-- The returned value will begin with ""http://"" or ""https://"" (depending on
+-- the config)
+vaultAddress :: VaultConfig -> VaultAddress
+vaultAddress VaultConfig{_VaultConfig_ListenAddress} =
+    VaultAddress ("http://" `T.append` _VaultConfig_ListenAddress)
+
+readVaultBackendConfig :: FilePath -> IO VaultBackendConfig
+readVaultBackendConfig file = do
+    fileContents <- BL.readFile file
+    case eitherDecode' fileContents of
+        Left err -> error $ "Error loading file " ++ show file ++ ": " ++ err
+        Right v -> pure v
+
+-- | File should have one line per key (blank lines are ignored)
+readVaultUnsealKeys :: FilePath -> IO [VaultUnsealKey]
+readVaultUnsealKeys file =
+    T.readFile file >>=
+        (pure . map VaultUnsealKey . (filter (not . T.null)) . map T.strip . T.lines)
+
+withVaultConfigFile :: VaultConfig -> (FilePath -> IO a) -> IO a
+withVaultConfigFile vaultConfig action = do
+    withSystemTempDirectory "hs_vault" $ \tmpDir -> do
+        let configFile = tmpDir </> "vault.cfg"
+        BL.writeFile configFile (encode vaultConfig)
+        action configFile
+
+data VaultServerProcess = VaultServerProcess
+    { vs_processHandle :: ProcessHandle
+    , vs_stdinH :: Handle
+    , vs_stdoutH :: Handle
+    , vs_stderrH :: Handle
+    }
+
+data VaultServerLaunchException
+    = VaultServerLaunchException_VaultStartTimeout
+    | VaultServerLaunchException_ConnectTimeout
+    | VaultServerLaunchException_ExecFailure IOException
+    | VaultServerLaunchException_ProcessFailure ExitCode Text
+    deriving (Show, Eq)
+
+instance Exception VaultServerLaunchException
+
+withVaultServerProcess :: Maybe FilePath -> FilePath -> VaultAddress -> IO a -> IO a
+withVaultServerProcess mbVaultExe vaultConfigFile addr act = do
+    bracket (launchVaultServerProcess mbVaultExe vaultConfigFile addr)
+        (shutdownVaultServerProcess)
+        (const act)
+
+launchVaultServerProcess :: Maybe FilePath -> FilePath -> VaultAddress -> IO VaultServerProcess
+launchVaultServerProcess mbVaultExe vaultConfigFile addr = do
+    bracketOnError
+        (execProcess vaultExe vaultConfigFile)
+        shutdownVaultServerProcess
+        $ \vs -> do
+            withAsync (waitUntilRunningThread (vs_stdoutH vs)) $ \waitUntilRunningA -> do
+                withAsync (checkProcessFailureThread vs) $ \startupErrorA -> do
+                    _ <- waitAnyCancel [waitUntilRunningA, startupErrorA]
+                    pure vs
+    where
+    vaultExe = fromMaybe "vault" mbVaultExe
+    waitUntilRunningThread stdoutH = do
+        withAsync (waitUntilVaultStarted stdoutH) $ \startA -> do
+            withAsync (timeout vaultStartTimeoutMilliseconds VaultServerLaunchException_VaultStartTimeout) $ \timeoutA -> do
+                _ <- waitAnyCancel [startA, timeoutA]
+                pure ()
+        withAsync waitUntilVaultConnect $ \connectA -> do
+            withAsync (timeout vaultConnectTimeoutMilliseconds VaultServerLaunchException_ConnectTimeout) $ \timeoutA -> do
+                _ <- waitAnyCancel [connectA, timeoutA]
+                pure ()
+    checkProcessFailureThread vs = do
+        mbExitCode <- getProcessExitCode (vs_processHandle vs)
+        case mbExitCode of
+            Just exitCode -> do
+                stderrText <- T.hGetContents (vs_stderrH vs)
+                throwIO $ VaultServerLaunchException_ProcessFailure exitCode stderrText
+            Nothing -> do
+                threadDelay (checkExitedSnoozeMilliseconds * 1000)
+                checkProcessFailureThread vs
+    vaultStartTimeoutMilliseconds = 10000
+    vaultConnectTimeoutMilliseconds = 10000
+    checkRunningSnoozeMilliseconds = 10
+    checkExitedSnoozeMilliseconds = 10
+    timeout milliseconds ex = do
+        threadDelay (milliseconds * 1000)
+        throwIO ex
+    waitUntilVaultStarted stdoutH = do
+        tryResult <- try $ T.hGetLine stdoutH
+        case tryResult of
+            Left (_ :: IOException) ->
+                -- Wait to be killed
+                forever (threadDelay 100000000)
+            Right ln -> do
+                -- This expects the vault program to output the string below to stdout. Verified to work for Vault versions [0.1.0 .. 0.6.0]
+                if vaultStartMessagePrefix `T.isPrefixOf` ln
+                    then pure ()
+                    else waitUntilVaultStarted stdoutH
+    vaultStartMessagePrefix = "==> Vault server started!"
+    waitUntilVaultConnect = do
+        running <- vaultIsRunning addr
+        if running
+            then pure ()
+            else do
+                threadDelay (checkRunningSnoozeMilliseconds * 1000)
+                waitUntilVaultConnect
+
+execProcess :: FilePath -> FilePath -> IO VaultServerProcess
+execProcess vaultExe vaultConfigFile = do
+    tryResult <- try $ createProcess $ (proc vaultExe ["server", "-config=" ++ vaultConfigFile])
+                                            { env = Just []
+                                            , std_in = CreatePipe
+                                            , std_out = CreatePipe
+                                            , std_err = CreatePipe
+                                            , close_fds = True
+                                            }
+    case tryResult of
+        Left ex -> throwIO $ VaultServerLaunchException_ExecFailure ex
+        Right (Just stdinH, Just stdoutH, Just stderrH, processHandle) ->
+            pure VaultServerProcess
+                { vs_processHandle = processHandle
+                , vs_stdinH = stdinH
+                , vs_stdoutH = stdoutH
+                , vs_stderrH = stderrH
+                }
+        Right _ -> error "execProcess: The Impossible Happened"
+
+shutdownVaultServerProcess :: VaultServerProcess -> IO ()
+shutdownVaultServerProcess vs = do
+    -- TODO Should send SIGINT instead
+    terminateProcess (vs_processHandle vs)
+    _ <- waitForProcess (vs_processHandle vs)
+    hClose (vs_stdinH vs)
+    hClose (vs_stdoutH vs)
+    hClose (vs_stderrH vs)
+
+vaultIsRunning :: VaultAddress -> IO Bool
+vaultIsRunning addr = do
+    (vaultHealth addr >> pure True) `catches`
+        [ Handler $ \(_ :: HttpException) -> pure False
+        , Handler $ \(_ :: VaultException) -> pure False
+        ]
diff --git a/test/test.hs b/test/test.hs
new file mode 100644
--- /dev/null
+++ b/test/test.hs
@@ -0,0 +1,187 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Main where
+
+import Data.Aeson
+import Data.List (sort)
+import GHC.Generics
+import System.Environment
+import System.IO.Temp (withSystemTempDirectory)
+import Test.Tasty.HUnit
+
+import Network.VaultTool
+import Network.VaultTool.VaultServerProcess
+
+withTempVaultBackend :: (VaultBackendConfig -> IO a) -> IO a
+withTempVaultBackend action = withSystemTempDirectory "hs_vault" $ \tmpDir -> do
+    let backendConfig = object
+            [ "file" .= object
+                [ "path" .= tmpDir
+                ]
+            ]
+    action backendConfig
+
+main :: IO ()
+main = withTempVaultBackend $ \vaultBackendConfig -> do
+    vaultExe <- lookupEnv "VAULT_EXE"
+
+    let cfg = vaultConfigDefaultAddress vaultBackendConfig
+        addr = vaultAddress cfg
+    withVaultConfigFile cfg $ \vaultConfigFile ->
+        withVaultServerProcess vaultExe vaultConfigFile addr $
+            talkToVault addr
+
+-- | The vault must be a newly created, non-initialized vault
+--
+-- TODO It would be better to break this into lots of individual unit tests
+-- instead of this one big-ass test
+talkToVault :: VaultAddress -> IO ()
+talkToVault addr = do
+    health <- vaultHealth addr
+    _VaultHealth_Initialized health @?= False
+
+    (unsealKeys, rootToken) <- vaultInit addr 4 2
+
+    length unsealKeys @?= 4
+
+    status0 <- vaultSealStatus addr
+    status0 @?= VaultSealStatus
+        { _VaultSealStatus_Sealed = True
+        , _VaultSealStatus_T = 2
+        , _VaultSealStatus_N = 4
+        , _VaultSealStatus_Progress = 0
+        }
+
+    status1 <- vaultUnseal addr (VaultUnseal_Key (unsealKeys !! 0))
+    status1 @?= VaultSealStatus
+        { _VaultSealStatus_Sealed = True
+        , _VaultSealStatus_T = 2
+        , _VaultSealStatus_N = 4
+        , _VaultSealStatus_Progress = 1
+        }
+
+    status2 <- vaultUnseal addr VaultUnseal_Reset
+    status2 @?= VaultSealStatus
+        { _VaultSealStatus_Sealed = True
+        , _VaultSealStatus_T = 2
+        , _VaultSealStatus_N = 4
+        , _VaultSealStatus_Progress = 0
+        }
+
+    status3 <- vaultUnseal addr (VaultUnseal_Key (unsealKeys !! 1))
+    status3 @?= VaultSealStatus
+        { _VaultSealStatus_Sealed = True
+        , _VaultSealStatus_T = 2
+        , _VaultSealStatus_N = 4
+        , _VaultSealStatus_Progress = 1
+        }
+
+    status4 <- vaultUnseal addr (VaultUnseal_Key (unsealKeys !! 2))
+    status4 @?= VaultSealStatus
+        { _VaultSealStatus_Sealed = False
+        , _VaultSealStatus_T = 2
+        , _VaultSealStatus_N = 4
+        , _VaultSealStatus_Progress = 0
+        }
+
+    conn <- connectToVault addr rootToken
+    allMounts <- vaultMounts conn
+
+    fmap _VaultMount_Type (lookup "cubbyhole/" allMounts) @?= Just "cubbyhole"
+    fmap _VaultMount_Type (lookup "secret/" allMounts) @?= Just "generic"
+    fmap _VaultMount_Type (lookup "sys/" allMounts) @?= Just "system"
+
+    _ <- vaultMountTune conn "cubbyhole"
+    _ <- vaultMountTune conn "secret"
+    _ <- vaultMountTune conn "sys"
+
+    vaultNewMount conn "mymount" VaultMount
+        { _VaultMount_Type = "generic"
+        , _VaultMount_Description = Just "blah blah blah"
+        , _VaultMount_Config = Just VaultMountConfig
+            { _VaultMountConfig_DefaultLeaseTtl = Just 42
+            , _VaultMountConfig_MaxLeaseTtl = Nothing
+            }
+        }
+
+    mounts2 <- vaultMounts conn
+    fmap _VaultMount_Description (lookup "mymount/" mounts2) @?= Just "blah blah blah"
+
+    t <- vaultMountTune conn "mymount"
+    _VaultMountConfig_DefaultLeaseTtl t @?= 42
+
+    vaultMountSetTune conn "mymount" VaultMountConfig
+        { _VaultMountConfig_DefaultLeaseTtl = Just 52
+        , _VaultMountConfig_MaxLeaseTtl = Nothing
+        }
+
+    t2 <- vaultMountTune conn "mymount"
+    _VaultMountConfig_DefaultLeaseTtl t2 @?= 52
+
+    vaultUnmount conn "mymount"
+
+    mounts3 <- vaultMounts conn
+    lookup "mymount/" mounts3 @?= Nothing
+
+    vaultWrite conn (VaultSecretPath "secret/big") (object ["A" .= 'a', "B" .= 'b'])
+
+    (_, r) <- vaultRead conn (VaultSecretPath "secret/big")
+    case r of
+        Left err -> assertFailure $ "Failed to parse secret/big: " ++ (show err)
+        Right x -> x @?= object ["A" .= 'a', "B" .= 'b']
+
+    vaultWrite conn (VaultSecretPath "secret/fun") (FunStuff "fun" [1, 2, 3])
+    (_, r2) <- vaultRead conn (VaultSecretPath "secret/fun")
+    case r2 of
+        Left err -> assertFailure $ "Failed to parse secret/big: " ++ (show err)
+        Right x -> x @?= (FunStuff "fun" [1, 2, 3])
+
+    (_, r3) <- vaultRead conn (VaultSecretPath "secret/big")
+    case r3 of
+        Left (v, _) -> v @?= object ["A" .= 'a', "B" .= 'b']
+        Right (x :: FunStuff) -> assertFailure $ "Somehow parsed an impossible value" ++ show x
+
+    vaultWrite conn (VaultSecretPath "secret/foo/bar/a") (object ["X" .= 'x'])
+    vaultWrite conn (VaultSecretPath "secret/foo/bar/b") (object ["X" .= 'x'])
+    vaultWrite conn (VaultSecretPath "secret/foo/bar/a/b/c/d/e/f/g") (object ["X" .= 'x'])
+    vaultWrite conn (VaultSecretPath "secret/foo/quack/duck") (object ["X" .= 'x'])
+
+    keys <- vaultList conn (VaultSecretPath "secret/")
+    assertBool "Secret in list" $ VaultSecretPath "secret/big" `elem` keys
+    vaultDelete conn (VaultSecretPath "secret/big")
+
+    keys2 <- vaultList conn (VaultSecretPath "secret")
+    assertBool "Secret not in list" $ not (VaultSecretPath "secret/big" `elem` keys2)
+
+    keys3 <- vaultListRecursive conn (VaultSecretPath "secret/foo/")
+    sort keys3 @?= sort
+        [ VaultSecretPath "secret/foo/bar/a"
+        , VaultSecretPath "secret/foo/bar/b"
+        , VaultSecretPath "secret/foo/bar/a/b/c/d/e/f/g"
+        , VaultSecretPath "secret/foo/quack/duck"
+        ]
+
+    vaultSeal conn
+
+    status5 <- vaultSealStatus addr
+    status5 @?= VaultSealStatus
+        { _VaultSealStatus_Sealed = True
+        , _VaultSealStatus_T = 2
+        , _VaultSealStatus_N = 4
+        , _VaultSealStatus_Progress = 0
+        }
+
+    health2 <- vaultHealth addr
+    _VaultHealth_Initialized health2 @?= True
+    _VaultHealth_Sealed health2 @?= True
+
+data FunStuff = FunStuff
+    { funString :: String
+    , funNumbers :: [Int]
+    }
+    deriving (Show, Eq, Generic)
+
+instance FromJSON FunStuff
+instance ToJSON FunStuff
diff --git a/vault-tool-server.cabal b/vault-tool-server.cabal
new file mode 100644
--- /dev/null
+++ b/vault-tool-server.cabal
@@ -0,0 +1,49 @@
+name:                vault-tool-server
+version:             0.0.0.1
+synopsis:            Utility library for spawning a HashiCorp Vault process
+description:         Utility library for spawning a HashiCorp Vault process
+license:             MIT
+license-file:        LICENSE
+author:              Bit Connor
+maintainer:          mutantlemon@gmail.com
+-- copyright:           
+category:            Network
+build-type:          Simple
+cabal-version:       >=1.10
+homepage:            https://github.com/bitc/hs-vault-tool
+bug-reports:         https://github.com/bitc/hs-vault-tool/issues
+extra-source-files:  README.md
+
+source-repository head
+  type:     git
+  location: https://github.com/bitc/hs-vault-tool.git
+
+library
+  exposed-modules:     Network.VaultTool.VaultServerProcess
+
+  build-depends:       base >=4.8 && <4.10,
+                       vault-tool,
+                       aeson,
+                       async,
+                       bytestring,
+                       filepath,
+                       http-client,
+                       process >= 1.2.0.0,
+                       temporary,
+                       text
+
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+
+test-suite test
+  default-language:    Haskell2010
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             test.hs
+
+  build-depends:       base >=4.8 && <4.10,
+                       vault-tool,
+                       vault-tool-server,
+                       aeson,
+                       tasty-hunit,
+                       temporary
