propellor spin

This commit is contained in:
Joey Hess 2014-07-06 15:56:56 -04:00
parent 9f781db6da
commit 58f79c12aa
Failed to extract signature
17 changed files with 248 additions and 195 deletions

View File

@ -72,14 +72,15 @@ hosts = -- (o) `
& Apt.buildDep ["git-annex"] `period` Daily & Apt.buildDep ["git-annex"] `period` Daily
-- Important stuff that needs not too much memory or CPU. -- Important stuff that needs not too much memory or CPU.
, standardSystem "diatom.kitenet.net" Stable "amd64" , let ctx = Context "diatom.kitenet.net "
in standardSystem "diatom.kitenet.net" Stable "amd64"
& ipv4 "107.170.31.195" & ipv4 "107.170.31.195"
& DigitalOcean.distroKernel & DigitalOcean.distroKernel
& Hostname.sane & Hostname.sane
& Ssh.hostKey SshDsa & Ssh.hostKey SshDsa ctx
& Ssh.hostKey SshRsa & Ssh.hostKey SshRsa ctx
& Ssh.hostKey SshEcdsa & Ssh.hostKey SshEcdsa ctx
& Apt.unattendedUpgrades & Apt.unattendedUpgrades
& Apt.serviceInstalledRunning "ntp" & Apt.serviceInstalledRunning "ntp"
& Postfix.satellite & Postfix.satellite
@ -89,9 +90,9 @@ hosts = -- (o) `
& Apt.serviceInstalledRunning "swapspace" & Apt.serviceInstalledRunning "swapspace"
& Apt.serviceInstalledRunning "apache2" & Apt.serviceInstalledRunning "apache2"
& File.hasPrivContent "/etc/ssl/certs/web.pem" & File.hasPrivContent "/etc/ssl/certs/web.pem" (Context "kitenet.net")
& File.hasPrivContent "/etc/ssl/private/web.pem" & File.hasPrivContent "/etc/ssl/private/web.pem" (Context "kitenet.net")
& File.hasPrivContent "/etc/ssl/certs/startssl.pem" & File.hasPrivContent "/etc/ssl/certs/startssl.pem" (Context "kitenet.net")
& Apache.modEnabled "ssl" & Apache.modEnabled "ssl"
& Apache.multiSSL & Apache.multiSSL
& File.ownerGroup "/srv/web" "joey" "joey" & File.ownerGroup "/srv/web" "joey" "joey"
@ -133,16 +134,17 @@ hosts = -- (o) `
& Dns.secondaryFor ["animx"] hosts "animx.eu.org" & Dns.secondaryFor ["animx"] hosts "animx.eu.org"
-- storage and backup server -- storage and backup server
, standardSystem "elephant.kitenet.net" Unstable "amd64" , let ctx = Context "elephant.kitenet.net"
in standardSystem "elephant.kitenet.net" Unstable "amd64"
& ipv4 "193.234.225.114" & ipv4 "193.234.225.114"
& Hostname.sane & Hostname.sane
& Postfix.satellite & Postfix.satellite
& Apt.unattendedUpgrades & Apt.unattendedUpgrades
& Ssh.hostKey SshDsa & Ssh.hostKey SshDsa ctx
& Ssh.hostKey SshRsa & Ssh.hostKey SshRsa ctx
& Ssh.hostKey SshEcdsa & Ssh.hostKey SshEcdsa ctx
& Ssh.keyImported SshRsa "joey" & Ssh.keyImported SshRsa "joey" ctx
-- PV-grub chaining -- PV-grub chaining
-- http://notes.pault.ag/linode-pv-grub-chainning/ -- http://notes.pault.ag/linode-pv-grub-chainning/
@ -263,13 +265,13 @@ standardSystem hn suite arch = host hn
& Apt.installed ["etckeeper"] & Apt.installed ["etckeeper"]
& Apt.installed ["ssh"] & Apt.installed ["ssh"]
& GitHome.installedFor "root" & GitHome.installedFor "root"
& User.hasSomePassword "root" & User.hasSomePassword "root" (Context hn)
-- Harden the system, but only once root's authorized_keys -- Harden the system, but only once root's authorized_keys
-- is safely in place. -- is safely in place.
& check (Ssh.hasAuthorizedKeys "root") & check (Ssh.hasAuthorizedKeys "root")
(Ssh.passwordAuthentication False) (Ssh.passwordAuthentication False)
& User.accountFor "joey" & User.accountFor "joey"
& User.hasSomePassword "joey" & User.hasSomePassword "joey" (Context hn)
& Sudo.enabledFor "joey" & Sudo.enabledFor "joey"
& GitHome.installedFor "joey" & GitHome.installedFor "joey"
& Apt.installed ["vim", "screen", "less"] & Apt.installed ["vim", "screen", "less"]

11
debian/changelog vendored
View File

@ -1,7 +1,14 @@
propellor (0.7.1) UNRELEASED; urgency=medium propellor (0.8.0) UNRELEASED; urgency=medium
* Completely reworked privdata storage. There is now a single file,
and each host is sent only the privdata that its Properties actually use.
To transition existing privdata, run propellor against a host and
watch out for the red failure messages, and run the suggested commands
to store the privdata using the new storage scheme.
* Add --edit to edit a privdata value in $EDITOR. * Add --edit to edit a privdata value in $EDITOR.
* Add --list-fields to list a host's currently set privdata fields. * Add --list-fields to list all currently set privdata fields.
* Fix randomHostKeys property to run openssh-server's postinst in a * Fix randomHostKeys property to run openssh-server's postinst in a
non-failing way. non-failing way.
* Hostname.sane now cleans up the 127.0.0.1 localhost line in /etc/hosts, * Hostname.sane now cleans up the 127.0.0.1 localhost line in /etc/hosts,

View File

@ -27,10 +27,11 @@ Since the propoellor git repository is public, you can't store
in cleartext private data such as passwords, ssh private keys, etc. in cleartext private data such as passwords, ssh private keys, etc.
Instead, `propellor --spin $host` looks for a Instead, `propellor --spin $host` looks for a
`~/.propellor/privdata/$host.gpg` file and if found decrypts it and sends `~/.propellor/privdata/privdata.gpg` file and if found decrypts it,
it to the remote host using ssh. This lets a remote host know its own extracts the private that that the $host needs, and sends it to to the
private data, without seeing all the rest. $host using ssh. This lets a host know its own private data, without
seeing all the rest.
To securely store private data, use: `propellor --set $host $field` To securely store private data, use: `propellor --set $field $context`
The field name will be something like 'Password "root"'; see PrivData.hs Propellor will tell you the details when you use a Property that needs
for available fields. PrivData.

View File

@ -105,6 +105,7 @@ Library
Propellor.Types Propellor.Types
Propellor.Types.OS Propellor.Types.OS
Propellor.Types.Dns Propellor.Types.Dns
Propellor.Types.PrivData
Other-Modules: Other-Modules:
Propellor.Types.Info Propellor.Types.Info
Propellor.CmdLine Propellor.CmdLine

View File

@ -27,10 +27,10 @@ usage = do
, " propellor hostname" , " propellor hostname"
, " propellor --spin hostname" , " propellor --spin hostname"
, " propellor --add-key keyid" , " propellor --add-key keyid"
, " propellor --set hostname field" , " propellor --set field context"
, " propellor --dump hostname field" , " propellor --dump field context"
, " propellor --edit hostname field" , " propellor --edit field context"
, " propellor --list-fields hostname" , " propellor --list-fields"
] ]
exitFailure exitFailure
@ -41,10 +41,10 @@ processCmdLine = go =<< getArgs
go ("--spin":h:[]) = return $ Spin h go ("--spin":h:[]) = return $ Spin h
go ("--boot":h:[]) = return $ Boot h go ("--boot":h:[]) = return $ Boot h
go ("--add-key":k:[]) = return $ AddKey k go ("--add-key":k:[]) = return $ AddKey k
go ("--set":h:f:[]) = withprivfield f (return . Set h) go ("--set":f:c:[]) = withprivfield f c Set
go ("--dump":h:f:[]) = withprivfield f (return . Dump h) go ("--dump":f:c:[]) = withprivfield f c Dump
go ("--edit":h:f:[]) = withprivfield f (return . Edit h) go ("--edit":f:c:[]) = withprivfield f c Edit
go ("--list-fields":h:[]) = return $ ListFields h go ("--list-fields":[]) = return ListFields
go ("--continue":s:[]) = case readish s of go ("--continue":s:[]) = case readish s of
Just cmdline -> return $ Continue cmdline Just cmdline -> return $ Continue cmdline
Nothing -> errorMessage "--continue serialization failure" Nothing -> errorMessage "--continue serialization failure"
@ -60,8 +60,8 @@ processCmdLine = go =<< getArgs
else return $ Run s else return $ Run s
go _ = usage go _ = usage
withprivfield s f = case readish s of withprivfield s c f = case readish s of
Just pf -> f pf Just pf -> return $ f pf (Context c)
Nothing -> errorMessage $ "Unknown privdata field " ++ s Nothing -> errorMessage $ "Unknown privdata field " ++ s
defaultMain :: [Host] -> IO () defaultMain :: [Host] -> IO ()
@ -73,10 +73,10 @@ defaultMain hostlist = do
go True cmdline go True cmdline
where where
go _ (Continue cmdline) = go False cmdline go _ (Continue cmdline) = go False cmdline
go _ (Set hn field) = setPrivData hn field go _ (Set field context) = setPrivData field context
go _ (Dump hn field) = dumpPrivData hn field go _ (Dump field context) = dumpPrivData field context
go _ (Edit hn field) = editPrivData hn field go _ (Edit field context) = editPrivData field context
go _ (ListFields hn) = listPrivDataFields hn go _ ListFields = listPrivDataFields
go _ (AddKey keyid) = addKey keyid go _ (AddKey keyid) = addKey keyid
go _ (Chain hn) = withhost hn $ \h -> do go _ (Chain hn) = withhost hn $ \h -> do
r <- runPropellor h $ ensureProperties $ hostProperties h r <- runPropellor h $ ensureProperties $ hostProperties h
@ -182,11 +182,11 @@ spin hn = do
void $ gitCommit [Param "--allow-empty", Param "-a", Param "-m", Param "propellor spin"] void $ gitCommit [Param "--allow-empty", Param "-a", Param "-m", Param "propellor spin"]
void $ boolSystem "git" [Param "push"] void $ boolSystem "git" [Param "push"]
cacheparams <- toCommand <$> sshCachingParams hn cacheparams <- toCommand <$> sshCachingParams hn
go cacheparams url =<< gpgDecrypt (privDataFile hn) go cacheparams url =<< gpgDecrypt privDataFile
where where
go cacheparams url privdata = withBothHandles createProcessSuccess (proc "ssh" $ cacheparams ++ [user, bootstrapcmd]) $ \(toh, fromh) -> do go cacheparams url privdata = withBothHandles createProcessSuccess (proc "ssh" $ cacheparams ++ [user, bootstrapcmd]) $ \(toh, fromh) -> do
let finish = do let finish = do
senddata toh (privDataFile hn) privDataMarker privdata senddata toh privDataLocal privDataMarker privdata
hClose toh hClose toh
-- Display remaining output. -- Display remaining output.

View File

@ -2,18 +2,20 @@
module Propellor.PrivData where module Propellor.PrivData where
import qualified Data.Map as M
import Control.Applicative import Control.Applicative
import System.FilePath import System.FilePath
import System.IO import System.IO
import System.Directory import System.Directory
import Data.Maybe import Data.Maybe
import Data.List import Data.Monoid
import Control.Monad import Control.Monad
import Control.Monad.IfElse import Control.Monad.IfElse
import "mtl" Control.Monad.Reader import "mtl" Control.Monad.Reader
import qualified Data.Map as M
import qualified Data.Set as S
import Propellor.Types import Propellor.Types
import Propellor.Types.Info
import Propellor.Message import Propellor.Message
import Utility.Monad import Utility.Monad
import Utility.PartialPrelude import Utility.PartialPrelude
@ -25,40 +27,57 @@ import Utility.Misc
import Utility.FileMode import Utility.FileMode
import Utility.Env import Utility.Env
-- | When the specified PrivDataField is available on the host Propellor -- | Allows a Property to access the value of a specific PrivDataField,
-- is provisioning, it provies the data to the action. Otherwise, it prints -- for use in a specific Context.
-- a message to help the user make the necessary private data available. --
withPrivData :: PrivDataField -> (String -> Propellor Result) -> Propellor Result -- Example use:
withPrivData field a = maybe missing a =<< liftIO (getPrivData field) --
-- > withPrivData (PrivFile pemfile) (Context "joeyh.name") $ \getdata ->
-- > property "joeyh.name ssl cert" $ getdata $ \privdata ->
-- > liftIO $ writeFile pemfile privdata
-- > where pemfile = "/etc/ssl/certs/web.pem"
--
-- Note that if the value is not available, the action is not run
-- and instead it prints a message to help the user make the necessary
-- private data available.
withPrivData
:: PrivDataField
-> Context
-> (((PrivData -> Propellor Result) -> Propellor Result) -> Property)
-> Property
withPrivData field context@(Context cname) mkprop = addinfo $ mkprop $ \a ->
maybe missing a =<< liftIO (getLocalPrivData field context)
where where
missing = do missing = liftIO $ do
host <- asks hostName warningMessage $ "Missing privdata " ++ show field ++ " (for " ++ cname ++ ")"
let host' = if ".docker" `isSuffixOf` host putStrLn $ "Fix this by running: propellor --set '" ++ show field ++ "' '" ++ cname ++ "'"
then "$parent_host"
else host
liftIO $ do
warningMessage $ "Missing privdata " ++ show field
putStrLn $ "Fix this by running: propellor --set "++host'++" '" ++ show field ++ "'"
return FailedChange return FailedChange
addinfo p = p { propertyInfo = propertyInfo p <> mempty { _privDataFields = S.singleton (field, context) } }
getPrivData :: PrivDataField -> IO (Maybe String) {- Gets the requested field's value, in the specified context if it's
getPrivData field = do - available, from the host's local privdata cache. -}
m <- catchDefaultIO Nothing $ readish <$> readFile privDataLocal getLocalPrivData :: PrivDataField -> Context -> IO (Maybe PrivData)
return $ maybe Nothing (M.lookup field) m getLocalPrivData field context =
getPrivData field context . fromMaybe M.empty <$> localcache
where
localcache = catchDefaultIO Nothing $ readish <$> readFile privDataLocal
setPrivData :: HostName -> PrivDataField -> IO () getPrivData :: PrivDataField -> Context -> (M.Map (PrivDataField, Context) PrivData) -> Maybe PrivData
setPrivData host field = do getPrivData field context = M.lookup (field, context)
setPrivData :: PrivDataField -> Context -> IO ()
setPrivData field context = do
putStrLn "Enter private data on stdin; ctrl-D when done:" putStrLn "Enter private data on stdin; ctrl-D when done:"
setPrivDataTo host field =<< hGetContentsStrict stdin setPrivDataTo field context =<< hGetContentsStrict stdin
dumpPrivData :: HostName -> PrivDataField -> IO () dumpPrivData :: PrivDataField -> Context -> IO ()
dumpPrivData host field = dumpPrivData field context =
maybe (error "Requested privdata is not set.") putStrLn maybe (error "Requested privdata is not set.") putStrLn
=<< getPrivDataFor host field =<< (getPrivData field context <$> decryptPrivData)
editPrivData :: HostName -> PrivDataField -> IO () editPrivData :: PrivDataField -> Context -> IO ()
editPrivData host field = do editPrivData field context = do
v <- getPrivDataFor host field v <- getPrivData field context <$> decryptPrivData
v' <- withTmpFile "propellorXXXX" $ \f h -> do v' <- withTmpFile "propellorXXXX" $ \f h -> do
hClose h hClose h
maybe noop (writeFileProtected f) v maybe noop (writeFileProtected f) v
@ -66,35 +85,30 @@ editPrivData host field = do
unlessM (boolSystem editor [File f]) $ unlessM (boolSystem editor [File f]) $
error "Editor failed; aborting." error "Editor failed; aborting."
readFile f readFile f
setPrivDataTo host field v' setPrivDataTo field context v'
listPrivDataFields :: HostName -> IO () listPrivDataFields :: IO ()
listPrivDataFields host = do listPrivDataFields = do
putStrLn (host ++ "'s currently set privdata fields:") putStrLn ("All currently set privdata fields:")
mapM_ list . M.keys =<< decryptPrivData host mapM_ list . M.keys =<< decryptPrivData
where where
list = putStrLn . ("\t" ++) . shellEscape . show list = putStrLn . ("\t" ++) . shellEscape . show
setPrivDataTo :: HostName -> PrivDataField -> String -> IO () setPrivDataTo :: PrivDataField -> Context -> PrivData -> IO ()
setPrivDataTo host field value = do setPrivDataTo field context value = do
makePrivDataDir makePrivDataDir
let f = privDataFile host m <- decryptPrivData
m <- decryptPrivData host let m' = M.insert (field, context) (chomp value) m
let m' = M.insert field (chomp value) m gpgEncrypt privDataFile (show m')
gpgEncrypt f (show m')
putStrLn "Private data set." putStrLn "Private data set."
void $ boolSystem "git" [Param "add", File f] void $ boolSystem "git" [Param "add", File privDataFile]
where where
chomp s chomp s
| end s == "\n" = chomp (beginning s) | end s == "\n" = chomp (beginning s)
| otherwise = s | otherwise = s
getPrivDataFor :: HostName -> PrivDataField -> IO (Maybe String) decryptPrivData :: IO (M.Map (PrivDataField, Context) PrivData)
getPrivDataFor host field = M.lookup field <$> decryptPrivData host decryptPrivData = fromMaybe M.empty . readish <$> gpgDecrypt privDataFile
decryptPrivData :: HostName -> IO (M.Map PrivDataField String)
decryptPrivData host = fromMaybe M.empty . readish
<$> gpgDecrypt (privDataFile host)
makePrivDataDir :: IO () makePrivDataDir :: IO ()
makePrivDataDir = createDirectoryIfMissing False privDataDir makePrivDataDir = createDirectoryIfMissing False privDataDir
@ -102,8 +116,8 @@ makePrivDataDir = createDirectoryIfMissing False privDataDir
privDataDir :: FilePath privDataDir :: FilePath
privDataDir = "privdata" privDataDir = "privdata"
privDataFile :: HostName -> FilePath privDataFile :: FilePath
privDataFile host = privDataDir </> host ++ ".gpg" privDataFile = privDataDir </> "privdata.gpg"
privDataLocal :: FilePath privDataLocal :: FilePath
privDataLocal = privDataDir </> "local" privDataLocal = privDataDir </> "local"

View File

@ -55,9 +55,10 @@ installed = Apt.installed ["docker.io"]
-- | Configures docker with an authentication file, so that images can be -- | Configures docker with an authentication file, so that images can be
-- pushed to index.docker.io. Optional. -- pushed to index.docker.io. Optional.
configured :: Property configured :: Property
configured = property "docker configured" go `requires` installed configured = prop `requires` installed
where where
go = withPrivData DockerAuthentication $ \cfg -> ensureProperty $ prop = withPrivData DockerAuthentication anyContext $ \getcfg ->
property "docker configured" $ getcfg $ \cfg -> ensureProperty $
"/root/.dockercfg" `File.hasContent` (lines cfg) "/root/.dockercfg" `File.hasContent` (lines cfg)
-- | A short descriptive name for a container. -- | A short descriptive name for a container.

View File

@ -17,16 +17,17 @@ f `hasContent` newcontent = fileProperty ("replace " ++ f)
-- --
-- The file's permissions are preserved if the file already existed. -- The file's permissions are preserved if the file already existed.
-- Otherwise, they're set to 600. -- Otherwise, they're set to 600.
hasPrivContent :: FilePath -> Property hasPrivContent :: FilePath -> Context -> Property
hasPrivContent f = property desc $ withPrivData (PrivFile f) $ \privcontent -> hasPrivContent f context = withPrivData (PrivFile f) context $ \getcontent ->
property desc $ getcontent $ \privcontent ->
ensureProperty $ fileProperty' writeFileProtected desc ensureProperty $ fileProperty' writeFileProtected desc
(\_oldcontent -> lines privcontent) f (\_oldcontent -> lines privcontent) f
where where
desc = "privcontent " ++ f desc = "privcontent " ++ f
-- | Leaves the file world-readable. -- | Leaves the file world-readable.
hasPrivContentExposed :: FilePath -> Property hasPrivContentExposed :: FilePath -> Context -> Property
hasPrivContentExposed f = hasPrivContent f `onChange` hasPrivContentExposed f context = hasPrivContent f context `onChange`
mode f (combineModes (ownerWriteMode:readModes)) mode f (combineModes (ownerWriteMode:readModes))
-- | Ensures that a line is present in a file, adding it to the end if not. -- | Ensures that a line is present in a file, adding it to the end if not.

View File

@ -9,6 +9,8 @@ import System.PosixCompat
installed :: Property installed :: Property
installed = Apt.installed ["gnupg"] installed = Apt.installed ["gnupg"]
type GpgKeyId = String
-- | Sets up a user with a gpg key from the privdata. -- | Sets up a user with a gpg key from the privdata.
-- --
-- Note that if a secret key is exported using gpg -a --export-secret-key, -- Note that if a secret key is exported using gpg -a --export-secret-key,
@ -21,14 +23,15 @@ installed = Apt.installed ["gnupg"]
-- The GpgKeyId does not have to be a numeric id; it can just as easily -- The GpgKeyId does not have to be a numeric id; it can just as easily
-- be a description of the key. -- be a description of the key.
keyImported :: GpgKeyId -> UserName -> Property keyImported :: GpgKeyId -> UserName -> Property
keyImported keyid user = flagFile' (property desc go) genflag keyImported keyid user = flagFile' prop genflag
`requires` installed `requires` installed
where where
desc = user ++ " has gpg key " ++ show keyid desc = user ++ " has gpg key " ++ show keyid
genflag = do genflag = do
d <- dotDir user d <- dotDir user
return $ d </> ".propellor-imported-keyid-" ++ keyid return $ d </> ".propellor-imported-keyid-" ++ keyid
go = withPrivData (GpgKey keyid) $ \key -> makeChange $ prop = withPrivData GpgKey (Context keyid) $ \getkey ->
property desc $ getkey $ \key -> makeChange $
withHandle StdinHandle createProcessSuccess withHandle StdinHandle createProcessSuccess
(proc "su" ["-c", "gpg --import", user]) $ \h -> do (proc "su" ["-c", "gpg --import", user]) $ \h -> do
fileEncoding h fileEncoding h

View File

@ -25,5 +25,6 @@ providerFor users baseurl = propertyList desc $
-- the identitites directory controls access, so open up -- the identitites directory controls access, so open up
-- file mode -- file mode
identfile u = File.hasPrivContentExposed $ identfile u = File.hasPrivContentExposed
concat $ [ "/var/lib/simpleid/identities/", u, ".identity" ] (concat [ "/var/lib/simpleid/identities/", u, ".identity" ])
(Context baseurl)

View File

@ -23,29 +23,25 @@ builddir = gitbuilderdir </> "build"
type TimeOut = String -- eg, 5h type TimeOut = String -- eg, 5h
autobuilder :: CronTimes -> TimeOut -> Bool -> Property autobuilder :: Architecture -> CronTimes -> TimeOut -> Property
autobuilder crontimes timeout rsyncupload = combineProperties "gitannexbuilder" autobuilder arch crontimes timeout = combineProperties "gitannexbuilder"
[ Apt.serviceInstalledRunning "cron" [ Apt.serviceInstalledRunning "cron"
, Cron.niceJob "gitannexbuilder" crontimes builduser gitbuilderdir $ , Cron.niceJob "gitannexbuilder" crontimes builduser gitbuilderdir $
"git pull ; timeout " ++ timeout ++ " ./autobuild" "git pull ; timeout " ++ timeout ++ " ./autobuild"
-- The builduser account does not have a password set, -- The builduser account does not have a password set,
-- instead use the password privdata to hold the rsync server -- instead use the password privdata to hold the rsync server
-- password used to upload the built image. -- password used to upload the built image.
, property "rsync password" $ do , withPrivData (Password builduser) context $ \getpw ->
let f = homedir </> "rsyncpassword" property "rsync password" $ getpw $ \pw -> do
if rsyncupload oldpw <- liftIO $ catchDefaultIO "" $
then withPrivData (Password builduser) $ \p -> do readFileStrict pwfile
oldp <- liftIO $ catchDefaultIO "" $ if pw /= oldpw
readFileStrict f then makeChange $ writeFile pwfile pw
if p /= oldp
then makeChange $ writeFile f p
else noChange else noChange
else do
ifM (liftIO $ doesFileExist f)
( noChange
, makeChange $ writeFile f "no password configured"
)
] ]
where
context = Context ("gitannexbuilder " ++ arch)
pwfile = homedir </> "rsyncpassword"
tree :: Architecture -> Property tree :: Architecture -> Property
tree buildarch = combineProperties "gitannexbuilder tree" tree buildarch = combineProperties "gitannexbuilder tree"
@ -101,13 +97,13 @@ standardAutoBuilderContainer dockerImage arch buildminute timeout = Docker.conta
& User.accountFor builduser & User.accountFor builduser
& tree arch & tree arch
& buildDepsApt & buildDepsApt
& autobuilder (show buildminute ++ " * * * *") timeout True & autobuilder arch (show buildminute ++ " * * * *") timeout
androidAutoBuilderContainer :: (System -> Docker.Image) -> Cron.CronTimes -> TimeOut -> Host androidAutoBuilderContainer :: (System -> Docker.Image) -> Cron.CronTimes -> TimeOut -> Host
androidAutoBuilderContainer dockerImage crontimes timeout = androidAutoBuilderContainer dockerImage crontimes timeout =
androidContainer dockerImage "android-git-annex-builder" (tree "android") builddir androidContainer dockerImage "android-git-annex-builder" (tree "android") builddir
& Apt.unattendedUpgrades & Apt.unattendedUpgrades
& autobuilder crontimes timeout True & autobuilder "android" crontimes timeout
-- Android is cross-built in a Debian i386 container, using the Android NDK. -- Android is cross-built in a Debian i386 container, using the Android NDK.
androidContainer :: (System -> Docker.Image) -> Docker.ContainerName -> Property -> FilePath -> Host androidContainer :: (System -> Docker.Image) -> Docker.ContainerName -> Property -> FilePath -> Host
@ -154,7 +150,7 @@ armelCompanionContainer dockerImage = Docker.container "armel-git-annex-builder-
-- The armel builder can ssh to this companion. -- The armel builder can ssh to this companion.
& Docker.expose "22" & Docker.expose "22"
& Apt.serviceInstalledRunning "ssh" & Apt.serviceInstalledRunning "ssh"
& Ssh.authorizedKeys builduser & Ssh.authorizedKeys builduser (Context "armel-git-annex-builder")
armelAutoBuilderContainer :: (System -> Docker.Image) -> Cron.CronTimes -> TimeOut -> Host armelAutoBuilderContainer :: (System -> Docker.Image) -> Cron.CronTimes -> TimeOut -> Host
armelAutoBuilderContainer dockerImage crontimes timeout = Docker.container "armel-git-annex-builder" armelAutoBuilderContainer dockerImage crontimes timeout = Docker.container "armel-git-annex-builder"
@ -172,9 +168,9 @@ armelAutoBuilderContainer dockerImage crontimes timeout = Docker.container "arme
-- git-annex/standalone/linux/install-haskell-packages -- git-annex/standalone/linux/install-haskell-packages
-- which is not fully automated.) -- which is not fully automated.)
& buildDepsNoHaskellLibs & buildDepsNoHaskellLibs
& autobuilder crontimes timeout True & autobuilder "armel" crontimes timeout
`requires` tree "armel" `requires` tree "armel"
& Ssh.keyImported SshRsa builduser & Ssh.keyImported SshRsa builduser (Context "armel-git-annex-builder")
& trivial writecompanionaddress & trivial writecompanionaddress
where where
writecompanionaddress = scriptProperty writecompanionaddress = scriptProperty

View File

@ -29,7 +29,7 @@ oldUseNetServer hosts = propertyList ("olduse.net server")
[ "--repository=sftp://2318@usw-s002.rsync.net/~/olduse.net" [ "--repository=sftp://2318@usw-s002.rsync.net/~/olduse.net"
, "--client-name=spool" , "--client-name=spool"
] Obnam.OnlyClient ] Obnam.OnlyClient
`requires` Ssh.keyImported SshRsa "root" `requires` Ssh.keyImported SshRsa "root" (Context "olduse.net")
`requires` Ssh.knownHost hosts "usw-s002.rsync.net" "root" `requires` Ssh.knownHost hosts "usw-s002.rsync.net" "root"
, check (not . isSymbolicLink <$> getSymbolicLinkStatus newsspool) $ , check (not . isSymbolicLink <$> getSymbolicLinkStatus newsspool) $
property "olduse.net spool in place" $ makeChange $ do property "olduse.net spool in place" $ makeChange $ do
@ -97,7 +97,7 @@ kgbServer = withOS desc $ \o -> case o of
(Just (System (Debian Unstable) _)) -> (Just (System (Debian Unstable) _)) ->
ensureProperty $ propertyList desc ensureProperty $ propertyList desc
[ Apt.serviceInstalledRunning "kgb-bot" [ Apt.serviceInstalledRunning "kgb-bot"
, File.hasPrivContent "/etc/kgb-bot/kgb.conf" , File.hasPrivContent "/etc/kgb-bot/kgb.conf" anyContext
`onChange` Service.restarted "kgb-bot" `onChange` Service.restarted "kgb-bot"
, "/etc/default/kgb-bot" `File.containsLine` "BOT_ENABLED=1" , "/etc/default/kgb-bot" `File.containsLine` "BOT_ENABLED=1"
`describe` "kgb bot enabled" `describe` "kgb bot enabled"
@ -108,17 +108,19 @@ kgbServer = withOS desc $ \o -> case o of
desc = "kgb.kitenet.net setup" desc = "kgb.kitenet.net setup"
mumbleServer :: [Host] -> Property mumbleServer :: [Host] -> Property
mumbleServer hosts = combineProperties "mumble.debian.net" mumbleServer hosts = combineProperties hn
[ Apt.serviceInstalledRunning "mumble-server" [ Apt.serviceInstalledRunning "mumble-server"
, Obnam.latestVersion , Obnam.latestVersion
, Obnam.backup "/var/lib/mumble-server" "55 5 * * *" , Obnam.backup "/var/lib/mumble-server" "55 5 * * *"
[ "--repository=sftp://joey@turtle.kitenet.net/~/lib/backup/mumble.debian.net.obnam" [ "--repository=sftp://joey@turtle.kitenet.net/~/lib/backup/" ++ hn ++ ".obnam"
, "--client-name=mumble" , "--client-name=mumble"
] Obnam.OnlyClient ] Obnam.OnlyClient
`requires` Ssh.keyImported SshRsa "root" `requires` Ssh.keyImported SshRsa "root" (Context hn)
`requires` Ssh.knownHost hosts "turtle.kitenet.net" "root" `requires` Ssh.knownHost hosts "turtle.kitenet.net" "root"
, trivial $ cmdProperty "chown" ["-R", "mumble-server:mumble-server", "/var/lib/mumble-server"] , trivial $ cmdProperty "chown" ["-R", "mumble-server:mumble-server", "/var/lib/mumble-server"]
] ]
where
hn = "mumble.debian.net"
obnamLowMem :: Property obnamLowMem :: Property
obnamLowMem = combineProperties "obnam tuned for low memory use" obnamLowMem = combineProperties "obnam tuned for low memory use"
@ -141,16 +143,16 @@ gitServer hosts = propertyList "git.kitenet.net setup"
, "--client-name=wren" , "--client-name=wren"
] Obnam.OnlyClient ] Obnam.OnlyClient
`requires` Gpg.keyImported "1B169BE1" "root" `requires` Gpg.keyImported "1B169BE1" "root"
`requires` Ssh.keyImported SshRsa "root" `requires` Ssh.keyImported SshRsa "root" (Context "git.kitenet.net")
`requires` Ssh.knownHost hosts "usw-s002.rsync.net" "root" `requires` Ssh.knownHost hosts "usw-s002.rsync.net" "root"
`requires` Ssh.authorizedKeys "family" `requires` Ssh.authorizedKeys "family" (Context "git.kitenet.net")
`requires` User.accountFor "family" `requires` User.accountFor "family"
, Apt.installed ["git", "rsync", "gitweb"] , Apt.installed ["git", "rsync", "gitweb"]
-- backport avoids channel flooding on branch merge -- backport avoids channel flooding on branch merge
, Apt.installedBackport ["kgb-client"] , Apt.installedBackport ["kgb-client"]
-- backport supports ssh event notification -- backport supports ssh event notification
, Apt.installedBackport ["git-annex"] , Apt.installedBackport ["git-annex"]
, File.hasPrivContentExposed "/etc/kgb-bot/kgb-client.conf" , File.hasPrivContentExposed "/etc/kgb-bot/kgb-client.conf" anyContext
, toProp $ Git.daemonRunning "/srv/git" , toProp $ Git.daemonRunning "/srv/git"
, "/etc/gitweb.conf" `File.containsLines` , "/etc/gitweb.conf" `File.containsLines`
[ "$projectroot = '/srv/git';" [ "$projectroot = '/srv/git';"
@ -202,7 +204,7 @@ annexWebSite hosts origin hn uuid remotes = propertyList (hn ++" website using g
dir = "/srv/web/" ++ hn dir = "/srv/web/" ++ hn
postupdatehook = dir </> ".git/hooks/post-update" postupdatehook = dir </> ".git/hooks/post-update"
setup = userScriptProperty "joey" setupscript setup = userScriptProperty "joey" setupscript
`requires` Ssh.keyImported SshRsa "joey" `requires` Ssh.keyImported SshRsa "joey" (Context hn)
`requires` Ssh.knownHost hosts "turtle.kitenet.net" "joey" `requires` Ssh.knownHost hosts "turtle.kitenet.net" "joey"
setupscript = setupscript =
[ "cd " ++ shellEscape dir [ "cd " ++ shellEscape dir
@ -270,9 +272,9 @@ mainhttpscert True =
gitAnnexDistributor :: Property gitAnnexDistributor :: Property
gitAnnexDistributor = combineProperties "git-annex distributor, including rsync server and signer" gitAnnexDistributor = combineProperties "git-annex distributor, including rsync server and signer"
[ Apt.installed ["rsync"] [ Apt.installed ["rsync"]
, File.hasPrivContent "/etc/rsyncd.conf" , File.hasPrivContent "/etc/rsyncd.conf" (Context "git-annex distributor")
`onChange` Service.restarted "rsync" `onChange` Service.restarted "rsync"
, File.hasPrivContent "/etc/rsyncd.secrets" , File.hasPrivContent "/etc/rsyncd.secrets" (Context "git-annex distributor")
`onChange` Service.restarted "rsync" `onChange` Service.restarted "rsync"
, "/etc/default/rsync" `File.containsLine` "RSYNC_ENABLE=true" , "/etc/default/rsync" `File.containsLine` "RSYNC_ENABLE=true"
`onChange` Service.running "rsync" `onChange` Service.running "rsync"
@ -315,7 +317,7 @@ ircBouncer = propertyList "IRC bouncer"
[ Apt.installed ["znc"] [ Apt.installed ["znc"]
, User.accountFor "znc" , User.accountFor "znc"
, File.dirExists (parentDir conf) , File.dirExists (parentDir conf)
, File.hasPrivContent conf , File.hasPrivContent conf anyContext
, File.ownerGroup conf "znc" "znc" , File.ownerGroup conf "znc" "znc"
, Cron.job "znconboot" "@reboot" "znc" "~" "znc" , Cron.job "znconboot" "@reboot" "znc" "~" "znc"
-- ensure running if it was not already -- ensure running if it was not already
@ -341,7 +343,7 @@ githubBackup :: Property
githubBackup = propertyList "github-backup box" githubBackup = propertyList "github-backup box"
[ Apt.installed ["github-backup", "moreutils"] [ Apt.installed ["github-backup", "moreutils"]
, let f = "/home/joey/.github-keys" , let f = "/home/joey/.github-keys"
in File.hasPrivContent f in File.hasPrivContent f anyContext
`onChange` File.ownerGroup f "joey" "joey" `onChange` File.ownerGroup f "joey" "joey"
] ]

View File

@ -75,40 +75,41 @@ randomHostKeys = flagFile prop "/etc/ssh/.unique_host_keys"
ensureProperty $ scriptProperty ensureProperty $ scriptProperty
[ "DPKG_MAINTSCRIPT_NAME=postinst DPKG_MAINTSCRIPT_PACKAGE=openssh-server /var/lib/dpkg/info/openssh-server.postinst configure" ] [ "DPKG_MAINTSCRIPT_NAME=postinst DPKG_MAINTSCRIPT_PACKAGE=openssh-server /var/lib/dpkg/info/openssh-server.postinst configure" ]
-- | Sets ssh host keys from the site's PrivData. -- | Sets ssh host keys.
-- hostKey :: SshKeyType -> Context -> Property
-- (Uses a null username for host keys.) hostKey keytype context = combineProperties desc
hostKey :: SshKeyType -> Property [ installkey (SshPubKey keytype "") (install writeFile ".pub")
hostKey keytype = combineProperties desc , installkey (SshPrivKey keytype "") (install writeFileProtected "")
[ property desc (install writeFile (SshPubKey keytype "") ".pub")
, property desc (install writeFileProtected (SshPrivKey keytype "") "")
] ]
`onChange` restartSshd `onChange` restartSshd
where where
desc = "known ssh host key (" ++ fromKeyType keytype ++ ")" desc = "known ssh host key (" ++ fromKeyType keytype ++ ")"
install writer p ext = withPrivData p $ \key -> do installkey p a = withPrivData p context $ \getkey ->
property desc $ getkey a
install writer ext key = do
let f = "/etc/ssh/ssh_host_" ++ fromKeyType keytype ++ "_key" ++ ext let f = "/etc/ssh/ssh_host_" ++ fromKeyType keytype ++ "_key" ++ ext
s <- liftIO $ readFileStrict f s <- liftIO $ readFileStrict f
if s == key if s == key
then noChange then noChange
else makeChange $ writer f key else makeChange $ writer f key
-- | Sets up a user with a ssh private key and public key pair -- | Sets up a user with a ssh private key and public key pair from the
-- from the site's PrivData. -- PrivData.
keyImported :: SshKeyType -> UserName -> Property keyImported :: SshKeyType -> UserName -> Context -> Property
keyImported keytype user = combineProperties desc keyImported keytype user context = combineProperties desc
[ property desc (install writeFile (SshPubKey keytype user) ".pub") [ installkey (SshPubKey keytype user) (install writeFile ".pub")
, property desc (install writeFileProtected (SshPrivKey keytype user) "") , installkey (SshPrivKey keytype user) (install writeFileProtected "")
] ]
where where
desc = user ++ " has ssh key (" ++ fromKeyType keytype ++ ")" desc = user ++ " has ssh key (" ++ fromKeyType keytype ++ ")"
install writer p ext = do installkey p a = withPrivData p context $ \getkey ->
property desc $ getkey a
install writer ext key = do
f <- liftIO $ keyfile ext f <- liftIO $ keyfile ext
ifM (liftIO $ doesFileExist f) ifM (liftIO $ doesFileExist f)
( noChange ( noChange
, ensureProperties , ensureProperties
[ property desc $ [ property desc $ makeChange $ do
withPrivData p $ \key -> makeChange $ do
createDirectoryIfMissing True (takeDirectory f) createDirectoryIfMissing True (takeDirectory f)
writer f key writer f key
, File.ownerGroup f user user , File.ownerGroup f user user
@ -143,9 +144,9 @@ knownHost hosts hn user = property desc $
return FailedChange return FailedChange
-- | Makes a user have authorized_keys from the PrivData -- | Makes a user have authorized_keys from the PrivData
authorizedKeys :: UserName -> Property authorizedKeys :: UserName -> Context -> Property
authorizedKeys user = property (user ++ " has authorized_keys") $ authorizedKeys user context = withPrivData (SshAuthorizedKeys user) context $ \get ->
withPrivData (SshAuthorizedKeys user) $ \v -> do property (user ++ " has authorized_keys") $ get $ \v -> do
f <- liftIO $ dotFile "authorized_keys" user f <- liftIO $ dotFile "authorized_keys" user
liftIO $ do liftIO $ do
createDirectoryIfMissing True (takeDirectory f) createDirectoryIfMissing True (takeDirectory f)

View File

@ -24,13 +24,14 @@ nuked user _ = check (isJust <$> catchMaybeIO (homedir user)) $ cmdProperty "use
-- | Only ensures that the user has some password set. It may or may -- | Only ensures that the user has some password set. It may or may
-- not be the password from the PrivData. -- not be the password from the PrivData.
hasSomePassword :: UserName -> Property hasSomePassword :: UserName -> Context -> Property
hasSomePassword user = check ((/= HasPassword) <$> getPasswordStatus user) $ hasSomePassword user context = check ((/= HasPassword) <$> getPasswordStatus user) $
hasPassword user hasPassword user context
hasPassword :: UserName -> Property hasPassword :: UserName -> Context -> Property
hasPassword user = property (user ++ " has password") $ hasPassword user context = withPrivData (Password user) context $ \getpassword ->
withPrivData (Password user) $ \password -> makeChange $ property (user ++ " has password") $
getpassword $ \password -> makeChange $
withHandle StdinHandle createProcessSuccess withHandle StdinHandle createProcessSuccess
(proc "chpasswd" []) $ \h -> do (proc "chpasswd" []) $ \h -> do
hPutStrLn h $ user ++ ":" ++ password hPutStrLn h $ user ++ ":" ++ password

View File

@ -17,7 +17,9 @@ module Propellor.Types
, ActionResult(..) , ActionResult(..)
, CmdLine(..) , CmdLine(..)
, PrivDataField(..) , PrivDataField(..)
, GpgKeyId , PrivData
, Context(..)
, anyContext
, SshKeyType(..) , SshKeyType(..)
, module Propellor.Types.OS , module Propellor.Types.OS
, module Propellor.Types.Dns , module Propellor.Types.Dns
@ -32,6 +34,7 @@ import "MonadCatchIO-transformers" Control.Monad.CatchIO
import Propellor.Types.Info import Propellor.Types.Info
import Propellor.Types.OS import Propellor.Types.OS
import Propellor.Types.Dns import Propellor.Types.Dns
import Propellor.Types.PrivData
-- | Everything Propellor knows about a system: Its hostname, -- | Everything Propellor knows about a system: Its hostname,
-- properties and other info. -- properties and other info.
@ -135,30 +138,12 @@ data CmdLine
= Run HostName = Run HostName
| Spin HostName | Spin HostName
| Boot HostName | Boot HostName
| Set HostName PrivDataField | Set PrivDataField Context
| Dump HostName PrivDataField | Dump PrivDataField Context
| Edit HostName PrivDataField | Edit PrivDataField Context
| ListFields HostName | ListFields
| AddKey String | AddKey String
| Continue CmdLine | Continue CmdLine
| Chain HostName | Chain HostName
| Docker HostName | Docker HostName
deriving (Read, Show, Eq) deriving (Read, Show, Eq)
-- | Note that removing or changing field names will break the
-- serialized privdata files, so don't do that!
-- It's fine to add new fields.
data PrivDataField
= DockerAuthentication
| SshPubKey SshKeyType UserName
| SshPrivKey SshKeyType UserName
| SshAuthorizedKeys UserName
| Password UserName
| PrivFile FilePath
| GpgKey GpgKeyId
deriving (Read, Show, Ord, Eq)
type GpgKeyId = String
data SshKeyType = SshRsa | SshDsa | SshEcdsa | SshEd25519
deriving (Read, Show, Ord, Eq)

View File

@ -1,6 +1,7 @@
module Propellor.Types.Info where module Propellor.Types.Info where
import Propellor.Types.OS import Propellor.Types.OS
import Propellor.Types.PrivData
import qualified Propellor.Types.Dns as Dns import qualified Propellor.Types.Dns as Dns
import qualified Data.Set as S import qualified Data.Set as S
@ -9,6 +10,7 @@ import Data.Monoid
-- | Information about a host. -- | Information about a host.
data Info = Info data Info = Info
{ _os :: Val System { _os :: Val System
, _privDataFields :: S.Set (PrivDataField, Context)
, _sshPubKey :: Val String , _sshPubKey :: Val String
, _dns :: S.Set Dns.Record , _dns :: S.Set Dns.Record
, _namedconf :: Dns.NamedConfMap , _namedconf :: Dns.NamedConfMap
@ -17,9 +19,10 @@ data Info = Info
deriving (Eq, Show) deriving (Eq, Show)
instance Monoid Info where instance Monoid Info where
mempty = Info mempty mempty mempty mempty mempty mempty = Info mempty mempty mempty mempty mempty mempty
mappend old new = Info mappend old new = Info
{ _os = _os old <> _os new { _os = _os old <> _os new
, _privDataFields = _privDataFields old <> _privDataFields new
, _sshPubKey = _sshPubKey old <> _sshPubKey new , _sshPubKey = _sshPubKey old <> _sshPubKey new
, _dns = _dns old <> _dns new , _dns = _dns old <> _dns new
, _namedconf = _namedconf old <> _namedconf new , _namedconf = _namedconf old <> _namedconf new

View File

@ -0,0 +1,34 @@
module Propellor.Types.PrivData where
import Propellor.Types.OS
-- | Note that removing or changing field names will break the
-- serialized privdata files, so don't do that!
-- It's fine to add new fields.
data PrivDataField
= DockerAuthentication
| SshPubKey SshKeyType UserName
| SshPrivKey SshKeyType UserName
| SshAuthorizedKeys UserName
| Password UserName
| PrivFile FilePath
| GpgKey
deriving (Read, Show, Ord, Eq)
-- | Context in which a PrivDataField is used.
--
-- Often this will be a domain name. For example,
-- Context "www.example.com" could be used for the SSL cert
-- for the web server serving that domain. Multiple hosts might
-- use that privdata.
newtype Context = Context String
deriving (Read, Show, Ord, Eq)
-- | Use when a PrivDataField is not dependent on any paricular context.
anyContext :: Context
anyContext = Context "any"
type PrivData = String
data SshKeyType = SshRsa | SshDsa | SshEcdsa | SshEd25519
deriving (Read, Show, Ord, Eq)