summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoey Hess2016-02-07 17:59:17 -0400
committerJoey Hess2016-02-07 17:59:17 -0400
commita568c7c0367b1ef6f01d0e8e638bb0f3fc7b2cb8 (patch)
treeda2b879619a60e4aa118a7d60316c5740593d6be
parentfcd124fe7e28e7abdfa8db15a4fbc3524aa98de0 (diff)
parent1f232b6be97c7e5480adb35811efdb9ab33ae5db (diff)
Merge branch 'joeyconfig'
-rw-r--r--config-joey.hs5
-rw-r--r--debian/changelog8
-rw-r--r--propellor.cabal1
-rw-r--r--src/Propellor/Property/Apache.hs162
-rw-r--r--src/Propellor/Property/Apt.hs2
-rw-r--r--src/Propellor/Property/LetsEncrypt.hs115
-rw-r--r--src/Propellor/Property/Obnam.hs67
-rw-r--r--src/Propellor/Property/SiteSpecific/JoeySites.hs3
8 files changed, 307 insertions, 56 deletions
diff --git a/config-joey.hs b/config-joey.hs
index 5e263de6..75333d78 100644
--- a/config-joey.hs
+++ b/config-joey.hs
@@ -261,19 +261,20 @@ kite = standardSystemUnhardened "kite.kitenet.net" Testing "amd64"
-- Since ssh password authentication is allowed:
& Fail2Ban.installed
& Obnam.backupEncrypted "/" (Cron.Times "33 1 * * *")
- [ "--repository=sftp://joey@eubackup.kitenet.net/~/lib/backup/kite.obnam"
+ [ "--repository=sftp://2318@usw-s002.rsync.net/~/kite.obnam"
, "--client-name=kitenet.net"
, "--exclude=/var/cache"
, "--exclude=/var/tmp"
, "--exclude=/home/joey/lib"
, "--exclude=.*/tmp/"
, "--one-file-system"
+ , Obnam.keepParam [Obnam.KeepDays 7, Obnam.KeepWeeks 4, Obnam.KeepMonths 6]
] Obnam.OnlyClient (Gpg.GpgKeyId "98147487")
`requires` Ssh.userKeys (User "root")
(Context "kite.kitenet.net")
[ (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC5Gza2sNqSKfNtUN4dN/Z3rlqw18nijmXFx6df2GtBoZbkIak73uQfDuZLP+AXlyfHocwdkdHEf/zrxgXS4EokQMGLZhJ37Pr3edrEn/NEnqroiffw7kyd7EqaziA6UOezcLTjWGv+Zqg9JhitYs4WWTpNzrPH3yQf1V9FunZnkzb4gJGndts13wGmPEwSuf+QHbgQvjMOMCJwWSNcJGdhDR66hFlxfG26xx50uIczXYAbgLfHp5W6WuR/lcaS9J6i7HAPwcsPDA04XDinrcpl29QwsMW1HyGS/4FSCgrDqNZ2jzP49Bka78iCLRqfl1efyYas/Zo1jQ0x+pxq2RMr root@kite")
]
- `requires` Ssh.knownHost hosts "eubackup.kitenet.net" (User "root")
+ `requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "root")
& Apt.serviceInstalledRunning "ntp"
& "/etc/timezone" `File.hasContent` ["US/Eastern"]
diff --git a/debian/changelog b/debian/changelog
index eadf65d2..3d18b8fb 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -3,6 +3,14 @@ propellor (2.15.4) UNRELEASED; urgency=medium
* Build /usr/src/propellor/propellor.git reproducibly,
which makes the whole Debian package build reproducibly.
Thanks, Sean Whitton.
+ * Obnam: To cause old generations to be forgotten, keepParam can be
+ passed to a backup property; this causes obnam forget to be run.
+ * Delete /etc/apt/apt.conf.d/50unattended-upgrades.ucf-dist when
+ unattended-upgrades is installed, to work around #812380 which results
+ in many warnings from apt, including in cron mails.
+ * Added Propellor.Property.LetsEncrypt
+ * Apache.httpsVirtualHost: New property, setting up a https vhost
+ with the certificate automatically obtained using letsencrypt.
-- Joey Hess <id@joeyh.name> Mon, 18 Jan 2016 13:15:30 -0400
diff --git a/propellor.cabal b/propellor.cabal
index 56b64ce7..fc2d3f1f 100644
--- a/propellor.cabal
+++ b/propellor.cabal
@@ -89,6 +89,7 @@ Library
Propellor.Property.Grub
Propellor.Property.Journald
Propellor.Property.Kerberos
+ Propellor.Property.LetsEncrypt
Propellor.Property.List
Propellor.Property.LightDM
Propellor.Property.Locale
diff --git a/src/Propellor/Property/Apache.hs b/src/Propellor/Property/Apache.hs
index 9e192e84..e841be9e 100644
--- a/src/Propellor/Property/Apache.hs
+++ b/src/Propellor/Property/Apache.hs
@@ -4,6 +4,7 @@ import Propellor.Base
import qualified Propellor.Property.File as File
import qualified Propellor.Property.Apt as Apt
import qualified Propellor.Property.Service as Service
+import qualified Propellor.Property.LetsEncrypt as LetsEncrypt
installed :: Property NoInfo
installed = Apt.installed ["apache2"]
@@ -14,48 +15,37 @@ restarted = Service.restarted "apache2"
reloaded :: Property NoInfo
reloaded = Service.reloaded "apache2"
--- | A basic virtual host, publishing a directory, and logging to
--- the combined apache log file.
-virtualHost :: HostName -> Port -> FilePath -> RevertableProperty NoInfo
-virtualHost hn (Port p) docroot = siteEnabled hn
- [ "<VirtualHost *:"++show p++">"
- , "ServerName "++hn++":"++show p
- , "DocumentRoot " ++ docroot
- , "ErrorLog /var/log/apache2/error.log"
- , "LogLevel warn"
- , "CustomLog /var/log/apache2/access.log combined"
- , "ServerSignature On"
- , "</VirtualHost>"
- ]
+type ConfigLine = String
-type ConfigFile = [String]
+type ConfigFile = [ConfigLine]
-siteEnabled :: HostName -> ConfigFile -> RevertableProperty NoInfo
-siteEnabled hn cf = enable <!> disable
- where
- enable = combineProperties ("apache site enabled " ++ hn)
- [ siteAvailable hn cf
+siteEnabled :: Domain -> ConfigFile -> RevertableProperty NoInfo
+siteEnabled domain cf = siteEnabled' domain cf <!> siteDisabled domain
+
+siteEnabled' :: Domain -> ConfigFile -> Property NoInfo
+siteEnabled' domain cf = combineProperties ("apache site enabled " ++ domain)
+ [ siteAvailable domain cf
+ `requires` installed
+ `onChange` reloaded
+ , check (not <$> isenabled)
+ (cmdProperty "a2ensite" ["--quiet", domain])
`requires` installed
`onChange` reloaded
- , check (not <$> isenabled)
- (cmdProperty "a2ensite" ["--quiet", hn])
- `requires` installed
- `onChange` reloaded
- ]
- disable = siteDisabled hn
- isenabled = boolSystem "a2query" [Param "-q", Param "-s", Param hn]
-
-siteDisabled :: HostName -> Property NoInfo
-siteDisabled hn = combineProperties
- ("apache site disabled " ++ hn)
- (map File.notPresent (siteCfg hn))
- `onChange` (cmdProperty "a2dissite" ["--quiet", hn] `assume` MadeChange)
+ ]
+ where
+ isenabled = boolSystem "a2query" [Param "-q", Param "-s", Param domain]
+
+siteDisabled :: Domain -> Property NoInfo
+siteDisabled domain = combineProperties
+ ("apache site disabled " ++ domain)
+ (map File.notPresent (siteCfg domain))
+ `onChange` (cmdProperty "a2dissite" ["--quiet", domain] `assume` MadeChange)
`requires` installed
`onChange` reloaded
-siteAvailable :: HostName -> ConfigFile -> Property NoInfo
-siteAvailable hn cf = combineProperties ("apache site available " ++ hn) $
- map (`File.hasContent` (comment:cf)) (siteCfg hn)
+siteAvailable :: Domain -> ConfigFile -> Property NoInfo
+siteAvailable domain cf = combineProperties ("apache site available " ++ domain) $
+ map (`File.hasContent` (comment:cf)) (siteCfg domain)
where
comment = "# deployed with propellor, do not modify"
@@ -86,12 +76,12 @@ listenPorts ps = "/etc/apache2/ports.conf" `File.hasContent` map portline ps
-- This is a list of config files because different versions of apache
-- use different filenames. Propellor simply writes them all.
-siteCfg :: HostName -> [FilePath]
-siteCfg hn =
+siteCfg :: Domain -> [FilePath]
+siteCfg domain =
-- Debian pre-2.4
- [ "/etc/apache2/sites-available/" ++ hn
+ [ "/etc/apache2/sites-available/" ++ domain
-- Debian 2.4+
- , "/etc/apache2/sites-available/" ++ hn ++ ".conf"
+ , "/etc/apache2/sites-available/" ++ domain ++ ".conf"
]
-- | Configure apache to use SNI to differentiate between
@@ -113,7 +103,7 @@ multiSSL = check (doesDirectoryExist "/etc/apache2/conf.d") $
--
-- Works with multiple versions of apache that have different ways to do
-- it.
-allowAll :: String
+allowAll :: ConfigLine
allowAll = unlines
[ "<IfVersion < 2.4>"
, "Order allow,deny"
@@ -123,3 +113,95 @@ allowAll = unlines
, "Require all granted"
, "</IfVersion>"
]
+
+-- | Config file fragment that can be inserted into a <VirtualHost>
+-- stanza to allow apache to display directory index icons.
+iconDir :: ConfigLine
+iconDir = unlines
+ [ "<Directory \"/usr/share/apache2/icons\">"
+ , "Options Indexes MultiViews"
+ , "AllowOverride None"
+ , allowAll
+ , " </Directory>"
+ ]
+
+type WebRoot = FilePath
+
+-- | A basic virtual host, publishing a directory, and logging to
+-- the combined apache log file. Not https capable.
+virtualHost :: Domain -> Port -> WebRoot -> RevertableProperty NoInfo
+virtualHost domain (Port p) docroot = virtualHost' domain (Port p) docroot []
+
+-- | Like `virtualHost` but with additional config lines added.
+virtualHost' :: Domain -> Port -> WebRoot -> [ConfigLine] -> RevertableProperty NoInfo
+virtualHost' domain (Port p) docroot addedcfg = siteEnabled domain $
+ [ "<VirtualHost *:"++show p++">"
+ , "ServerName "++domain++":"++show p
+ , "DocumentRoot " ++ docroot
+ , "ErrorLog /var/log/apache2/error.log"
+ , "LogLevel warn"
+ , "CustomLog /var/log/apache2/access.log combined"
+ , "ServerSignature On"
+ ]
+ ++ addedcfg ++
+ [ "</VirtualHost>"
+ ]
+
+-- | A virtual host using https, with the certificate obtained
+-- using `Propellor.Property.LetsEncrypt.letsEncrypt`.
+--
+-- http connections are redirected to https.
+--
+-- Example:
+--
+-- > httpsVirtualHost "example.com" "/var/www"
+-- > (LetsEncrypt.AgreeTos (Just "me@my.domain"))
+httpsVirtualHost :: Domain -> WebRoot -> LetsEncrypt.AgreeTOS -> Property NoInfo
+httpsVirtualHost domain docroot letos = httpsVirtualHost' domain docroot letos []
+
+-- | Like `httpsVirtualHost` but with additional config lines added.
+httpsVirtualHost' :: Domain -> WebRoot -> LetsEncrypt.AgreeTOS -> [ConfigLine] -> Property NoInfo
+httpsVirtualHost' domain docroot letos addedcfg = setup
+ `requires` modEnabled "rewrite"
+ `requires` modEnabled "ssl"
+ `before` LetsEncrypt.letsEncrypt letos domain docroot certinstaller
+ where
+ setup = siteEnabled' domain $
+ -- The sslconffile is only created after letsencrypt gets
+ -- the cert. The "*" is needed to make apache not error
+ -- when the file doesn't exist.
+ ("IncludeOptional " ++ sslconffile "*")
+ : vhost (Port 80)
+ [ "RewriteEngine On"
+ -- Pass through .well-known directory on http for the
+ -- letsencrypt acme challenge.
+ , "RewriteRule ^/.well-known/(.*) - [L]"
+ -- Everything else redirects to https
+ , "RewriteRule ^/(.*) https://" ++ domain ++ "/$1 [L,R,NE]"
+ ]
+ certinstaller _domain certfile privkeyfile chainfile _fullchainfile =
+ combineProperties (domain ++ " ssl cert installed")
+ [ File.dirExists (takeDirectory cf)
+ , File.hasContent cf $ vhost (Port 443)
+ [ "SSLEngine on"
+ , "SSLCertificateFile " ++ certfile
+ , "SSLCertificateKeyFile" ++ privkeyfile
+ , "SSLCertificateChainFile " ++ chainfile
+ ]
+ -- always reload; the cert has changed
+ , reloaded
+ ]
+ where
+ cf = sslconffile "letsencrypt"
+ sslconffile s = "/etc/apache2/sites-available/ssl/" ++ domain ++ "/" ++ s ++ ".conf"
+ vhost (Port p) ls =
+ [ "<VirtualHost *:"++show p++">"
+ , "ServerName "++domain++":"++show p
+ , "DocumentRoot " ++ docroot
+ , "ErrorLog /var/log/apache2/error.log"
+ , "LogLevel warn"
+ , "CustomLog /var/log/apache2/access.log combined"
+ , "ServerSignature On"
+ ] ++ ls ++ addedcfg ++
+ [ "</VirtualHost>"
+ ]
diff --git a/src/Propellor/Property/Apt.hs b/src/Propellor/Property/Apt.hs
index d16c4855..f5d08c1d 100644
--- a/src/Propellor/Property/Apt.hs
+++ b/src/Propellor/Property/Apt.hs
@@ -223,6 +223,8 @@ unattendedUpgrades = enable <!> disable
enable = setup True
`before` Service.running "cron"
`before` configure
+ -- work around http://bugs.debian.org/812380
+ `before` File.notPresent "/etc/apt/apt.conf.d/50unattended-upgrades.ucf-dist"
disable = setup False
setup enabled = (if enabled then installed else removed) ["unattended-upgrades"]
diff --git a/src/Propellor/Property/LetsEncrypt.hs b/src/Propellor/Property/LetsEncrypt.hs
new file mode 100644
index 00000000..651cffd9
--- /dev/null
+++ b/src/Propellor/Property/LetsEncrypt.hs
@@ -0,0 +1,115 @@
+-- | This module uses the letsencrypt reference client.
+
+module Propellor.Property.LetsEncrypt where
+
+import Propellor.Base
+import qualified Propellor.Property.Apt as Apt
+
+import System.Posix.Files
+
+installed :: Property NoInfo
+installed = Apt.installed ["letsencrypt"]
+
+-- | Tell the letsencrypt client that you agree with the Let's Encrypt
+-- Subscriber Agreement. Providing an email address is recommended,
+-- so that letcencrypt can contact you about problems.
+data AgreeTOS = AgreeTOS (Maybe Email)
+
+type Email = String
+
+type WebRoot = FilePath
+
+-- | Uses letsencrypt to obtain a certificate for a domain.
+--
+-- This should work with any web server, as long as letsencrypt can
+-- write its temp files to the web root. The letsencrypt client does
+-- not modify the web server's configuration in any way; instead the
+-- `CertInstaller` is used once the client has successfully obtained the
+-- certificate.
+--
+-- This also handles renewing the certificate, and the `CertInstaller` is
+-- also run after renewal. For renewel to work well, propellor needs to be
+-- run periodically (at least a couple times per month).
+--
+-- See `Propellor.Property.Apache.httpsVirtualHost` for a property built using this.
+letsEncrypt :: AgreeTOS -> Domain -> WebRoot -> CertInstaller -> Property NoInfo
+letsEncrypt tos domain = letsEncrypt' tos domain []
+
+-- | Like `letsEncrypt`, but the certificate can be obtained for multiple
+-- domains.
+letsEncrypt' :: AgreeTOS -> Domain -> [Domain] -> WebRoot -> CertInstaller -> Property NoInfo
+letsEncrypt' (AgreeTOS memail) domain domains webroot certinstaller =
+ prop `requires` installed
+ where
+ prop = property desc $ do
+ startstats <- liftIO getstats
+ (transcript, ok) <- liftIO $
+ processTranscript "letsencrypt" params Nothing
+ if ok
+ then do
+ endstats <- liftIO getstats
+ if startstats == endstats
+ then return NoChange
+ else ensureProperty certsinstalled
+ else do
+ liftIO $ hPutStr stderr transcript
+ return FailedChange
+
+ desc = "letsencrypt " ++ unwords alldomains
+ alldomains = domain : domains
+ params =
+ [ "certonly"
+ , "--agree-tos"
+ , case memail of
+ Just email -> "--email="++email
+ Nothing -> "--register-unsafely-without-email"
+ , "--webroot"
+ , "--webroot-path", webroot
+ , "--text"
+ , "--keep-until-expiring"
+ ] ++ map (\d -> "--domain="++d) alldomains
+
+ getstats = mapM statcertfiles alldomains
+ statcertfiles d = mapM statfile
+ [ certFile d
+ , privKeyFile d
+ , chainFile d
+ , fullChainFile d
+ ]
+ statfile f = catchMaybeIO $ do
+ s <- getFileStatus f
+ return (fileID s, deviceID s, fileMode s, fileSize s, modificationTime s)
+
+ certsinstalled = propertyList ("certs installed") $
+ flip map alldomains $ \d -> certinstaller d
+ (certFile d)
+ (privKeyFile d)
+ (chainFile d)
+ (fullChainFile d)
+
+-- | A property that installs a certificate, once letsencrypt obtains it.
+--
+-- For example, it could configure the web server to use the certificate
+-- files, and restart the web server.
+type CertInstaller = Domain -> CertFile -> PrivKeyFile -> ChainFile -> FullChainFile -> Property NoInfo
+
+-- | Locations of certificate files generated by lets encrypt.
+type CertFile = FilePath
+type PrivKeyFile = FilePath
+type ChainFile = FilePath
+type FullChainFile = FilePath
+
+liveCertDir :: Domain -> FilePath
+liveCertDir d = "/etc/letsencrypt/live" </> d
+
+certFile :: Domain -> FilePath
+certFile d = liveCertDir d </> "cert.pem"
+
+privKeyFile :: Domain -> FilePath
+privKeyFile d = liveCertDir d </> "privkey.pem"
+
+chainFile :: Domain -> FilePath
+chainFile d = liveCertDir d </> "chain.pem"
+
+fullChainFile :: Domain -> FilePath
+fullChainFile d = liveCertDir d </> "fullchain.pem"
diff --git a/src/Propellor/Property/Obnam.hs b/src/Propellor/Property/Obnam.hs
index 091a6d90..9a391967 100644
--- a/src/Propellor/Property/Obnam.hs
+++ b/src/Propellor/Property/Obnam.hs
@@ -25,9 +25,7 @@ data NumClients = OnlyClient | MultipleClients
--
-- So, this property can be used to deploy a directory of content
-- to a host, while also ensuring any changes made to it get backed up.
--- And since Obnam encrypts, just make this property depend on a gpg
--- key, and tell obnam to use the key, and your data will be backed
--- up securely. For example:
+-- For example:
--
-- > & Obnam.backup "/srv/git" "33 3 * * *"
-- > [ "--repository=sftp://2318@usw-s002.rsync.net/~/mygitrepos.obnam"
@@ -35,13 +33,16 @@ data NumClients = OnlyClient | MultipleClients
-- > `requires` Ssh.keyImported SshRsa "root" (Context hostname)
--
-- How awesome is that?
+--
+-- Note that this property does not make obnam encrypt the backup
+-- repository.
backup :: FilePath -> Cron.Times -> [ObnamParam] -> NumClients -> Property NoInfo
backup dir crontimes params numclients =
backup' dir crontimes params numclients
`requires` restored dir params
-- | Like backup, but the specified gpg key id is used to encrypt
--- the repository.
+-- the repository.
--
-- The gpg secret key will be automatically imported
-- into root's keyring using Propellor.Property.Gpg.keyImported
@@ -58,19 +59,29 @@ backup' dir crontimes params numclients = cronjob `describe` desc
where
desc = dir ++ " backed up by obnam"
cronjob = Cron.niceJob ("obnam_backup" ++ dir) crontimes (User "root") "/" $
- intercalate ";" $ catMaybes
+ unwords $ catMaybes
[ if numclients == OnlyClient
- then Just $ unwords $
- [ "obnam"
- , "force-lock"
- ] ++ map shellEscape params
+ -- forcelock fails if repo does not exist yet
+ then Just $ forcelock ++ " 2>/dev/null ;"
+ else Nothing
+ , Just backup
+ , if any isKeepParam params
+ then Just $ "&& " ++ forget
else Nothing
- , Just $ unwords $
- [ "obnam"
- , "backup"
- , shellEscape dir
- ] ++ map shellEscape params
]
+ forcelock = unwords $
+ [ "obnam"
+ , "force-lock"
+ ] ++ map shellEscape params
+ backup = unwords $
+ [ "obnam"
+ , "backup"
+ , shellEscape dir
+ ] ++ map shellEscape params
+ forget = unwords $
+ [ "obnam"
+ , "forget"
+ ] ++ map shellEscape params
-- | Restores a directory from an obnam backup.
--
@@ -107,5 +118,33 @@ restored dir params = property (dir ++ " restored by obnam") go
, return FailedChange
)
+-- | Policy for backup generations to keep. For example, KeepDays 30 will
+-- keep the latest backup for each day when a backup was made, and keep the
+-- last 30 such backups. When multiple KeepPolicies are combined together,
+-- backups meeting any policy are kept. See obnam's man page for details.
+data KeepPolicy
+ = KeepHours Int
+ | KeepDays Int
+ | KeepWeeks Int
+ | KeepMonths Int
+ | KeepYears Int
+
+-- | Constructs an ObnamParam that specifies which old backup generations
+-- to keep. By default, all generations are kept. However, when this parameter
+-- is passed to the `backup` or `backupEncrypted` properties, they will run
+-- obnam forget to clean out generations not specified here.
+keepParam :: [KeepPolicy] -> ObnamParam
+keepParam ps = "--keep=" ++ intercalate "," (map go ps)
+ where
+ go (KeepHours n) = mk n 'h'
+ go (KeepDays n) = mk n 'd'
+ go (KeepWeeks n) = mk n 'w'
+ go (KeepMonths n) = mk n 'm'
+ go (KeepYears n) = mk n 'y'
+ mk n c = show n ++ [c]
+
+isKeepParam :: ObnamParam -> Bool
+isKeepParam p = "--keep=" `isPrefixOf` p
+
installed :: Property NoInfo
installed = Apt.installed ["obnam"]
diff --git a/src/Propellor/Property/SiteSpecific/JoeySites.hs b/src/Propellor/Property/SiteSpecific/JoeySites.hs
index 7e6d3f8c..03f2efcb 100644
--- a/src/Propellor/Property/SiteSpecific/JoeySites.hs
+++ b/src/Propellor/Property/SiteSpecific/JoeySites.hs
@@ -140,6 +140,7 @@ oldUseNetServer hosts = propertyList "olduse.net server" $ props
[ "--repository=sftp://2318@usw-s002.rsync.net/~/olduse.net"
, "--client-name=spool"
, "--ssh-key=" ++ keyfile
+ , Obnam.keepParam [Obnam.KeepDays 30]
] Obnam.OnlyClient
`requires` Ssh.userKeyAt (Just keyfile)
(User "root")
@@ -194,6 +195,7 @@ mumbleServer hosts = combineProperties hn $ props
[ "--repository=sftp://2318@usw-s002.rsync.net/~/" ++ hn ++ ".obnam"
, "--ssh-key=" ++ sshkey
, "--client-name=mumble"
+ , Obnam.keepParam [Obnam.KeepDays 30]
] Obnam.OnlyClient
`requires` Ssh.userKeyAt (Just sshkey)
(User "root")
@@ -213,6 +215,7 @@ gitServer hosts = propertyList "git.kitenet.net setup" $ props
[ "--repository=sftp://2318@usw-s002.rsync.net/~/git.kitenet.net"
, "--ssh-key=" ++ sshkey
, "--client-name=wren" -- historical
+ , Obnam.keepParam [Obnam.KeepDays 30]
] Obnam.OnlyClient (Gpg.GpgKeyId "1B169BE1")
`requires` Ssh.userKeyAt (Just sshkey)
(User "root")