1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
|
module Propellor.Property.User where
import System.Posix
import Propellor.Base
import qualified Propellor.Property.File as File
data Eep = YesReallyDeleteHome
accountFor :: User -> Property DebianLike
accountFor user@(User u) = tightenTargets $ check nohomedir go
`describe` ("account for " ++ u)
where
nohomedir = isNothing <$> catchMaybeIO (homedir user)
go = cmdProperty "adduser"
[ "--disabled-password"
, "--gecos", ""
, u
]
systemAccountFor :: User -> Property DebianLike
systemAccountFor user@(User u) = systemAccountFor' user Nothing (Just (Group u))
systemAccountFor' :: User -> Maybe FilePath -> Maybe Group -> Property DebianLike
systemAccountFor' (User u) mhome mgroup = case mgroup of
Nothing -> prop
Just g -> prop
`requires` systemGroup g
`describe` ("system account for " ++ u)
where
prop = tightenTargets $ check nouser go
nouser = isNothing <$> catchMaybeIO (getUserEntryForName u)
go = cmdProperty "adduser" $
[ "--system", "--home" ]
++
maybe ["/nonexistent", "--no-create-home"] ( \h -> [h] ) mhome
++
maybe [] ( \(Group g) -> ["--ingroup", g] ) mgroup
++
[ "--shell", "/usr/bin/nologin"
, "--disabled-login"
, "--disabled-password"
, u
]
systemGroup :: Group -> Property UnixLike
systemGroup (Group g) = check nogroup go
`describe` ("system account for " ++ g)
where
nogroup = isNothing <$> catchMaybeIO (getGroupEntryForName g)
go = cmdProperty "addgroup"
[ "--system"
, g
]
-- | Removes user home directory!! Use with caution.
nuked :: User -> Eep -> Property Linux
nuked user@(User u) _ = tightenTargets $ check hashomedir go
`describe` ("nuked user " ++ u)
where
hashomedir = isJust <$> catchMaybeIO (homedir user)
go = cmdProperty "userdel"
[ "-r"
, u
]
-- | Only ensures that the user has some password set. It may or may
-- not be a password from the PrivData.
hasSomePassword :: User -> Property (HasInfo + DebianLike)
hasSomePassword user = hasSomePassword' user hostContext
-- | While hasSomePassword uses the name of the host as context,
-- this allows specifying a different context. This is useful when
-- you want to use the same password on multiple hosts, for example.
hasSomePassword' :: IsContext c => User -> c -> Property (HasInfo + DebianLike)
hasSomePassword' user context = check ((/= HasPassword) <$> getPasswordStatus user) $
hasPassword' user context
-- | Ensures that a user's password is set to a password from the PrivData.
-- (Will change any existing password.)
--
-- A user's password can be stored in the PrivData in either of two forms;
-- the full cleartext <Password> or a <CryptPassword> hash. The latter
-- is obviously more secure.
hasPassword :: User -> Property (HasInfo + DebianLike)
hasPassword user = hasPassword' user hostContext
hasPassword' :: IsContext c => User -> c -> Property (HasInfo + DebianLike)
hasPassword' (User u) context = go
`requires` shadowConfig True
where
go :: Property (HasInfo + UnixLike)
go = withSomePrivData srcs context $
property (u ++ " has password") . setPassword
srcs =
[ PrivDataSource (CryptPassword u)
"a crypt(3)ed password, which can be generated by, for example: perl -e 'print crypt(shift, q{$6$}.shift)' 'somepassword' 'somesalt'"
, PrivDataSource (Password u) ("a password for " ++ u)
]
setPassword :: (((PrivDataField, PrivData) -> Propellor Result) -> Propellor Result) -> Propellor Result
setPassword getpassword = getpassword $ go
where
go (Password user, password) = chpasswd (User user) (privDataVal password) []
go (CryptPassword user, hash) = chpasswd (User user) (privDataVal hash) ["--encrypted"]
go (f, _) = error $ "Unexpected type of privdata: " ++ show f
-- | Makes a user's password be the passed String. Highly insecure:
-- The password is right there in your config file for anyone to see!
hasInsecurePassword :: User -> String -> Property DebianLike
hasInsecurePassword u@(User n) p = go
`requires` shadowConfig True
where
go :: Property DebianLike
go = property (n ++ " has insecure password") $
chpasswd u p []
chpasswd :: User -> String -> [String] -> Propellor Result
chpasswd (User user) v ps = makeChange $ withHandle StdinHandle createProcessSuccess
(proc "chpasswd" ps) $ \h -> do
hPutStrLn h $ user ++ ":" ++ v
hClose h
lockedPassword :: User -> Property DebianLike
lockedPassword user@(User u) = tightenTargets $
check (not <$> isLockedPassword user) go
`describe` ("locked " ++ u ++ " password")
where
go = cmdProperty "passwd"
[ "--lock"
, u
]
data PasswordStatus = NoPassword | LockedPassword | HasPassword
deriving (Eq)
getPasswordStatus :: User -> IO PasswordStatus
getPasswordStatus (User u) = parse . words <$> readProcess "passwd" ["-S", u]
where
parse (_:"L":_) = LockedPassword
parse (_:"NP":_) = NoPassword
parse (_:"P":_) = HasPassword
parse _ = NoPassword
isLockedPassword :: User -> IO Bool
isLockedPassword user = (== LockedPassword) <$> getPasswordStatus user
homedir :: User -> IO FilePath
homedir (User user) = homeDirectory <$> getUserEntryForName user
hasGroup :: User -> Group -> Property DebianLike
hasGroup (User user) (Group group') = tightenTargets $ check test go
`describe` unwords ["user", user, "in group", group']
where
test = not . elem group' . words <$> readProcess "groups" [user]
go = cmdProperty "adduser"
[ user
, group'
]
-- | Gives a user access to the secondary groups, including audio and
-- video, that the OS installer normally gives a desktop user access to.
--
-- Note that some groups may only exit after installation of other
-- software. When a group does not exist yet, the user won't be added to it.
hasDesktopGroups :: User -> Property DebianLike
hasDesktopGroups user@(User u) = property' desc $ \o -> do
existinggroups <- map (fst . break (== ':')) . lines
<$> liftIO (readFile "/etc/group")
let toadd = filter (`elem` existinggroups) desktopgroups
ensureProperty o $ propertyList desc $ toProps $
map (hasGroup user . Group) toadd
where
desc = "user " ++ u ++ " is in standard desktop groups"
-- This list comes from user-setup's debconf
-- template named "passwd/user-default-groups"
desktopgroups =
[ "audio"
, "cdrom"
, "dip"
, "floppy"
, "video"
, "plugdev"
, "netdev"
, "scanner"
, "bluetooth"
, "debian-tor"
, "lpadmin"
]
-- | Controls whether shadow passwords are enabled or not.
shadowConfig :: Bool -> Property DebianLike
shadowConfig True = tightenTargets $ check (not <$> shadowExists)
(cmdProperty "shadowconfig" ["on"])
`describe` "shadow passwords enabled"
shadowConfig False = tightenTargets $ check shadowExists
(cmdProperty "shadowconfig" ["off"])
`describe` "shadow passwords disabled"
shadowExists :: IO Bool
shadowExists = doesFileExist "/etc/shadow"
-- | Ensures that a user has a specified login shell, and that the shell
-- is enabled in /etc/shells.
hasLoginShell :: User -> FilePath -> Property DebianLike
hasLoginShell user loginshell = shellSetTo user loginshell `requires` shellEnabled loginshell
shellSetTo :: User -> FilePath -> Property DebianLike
shellSetTo (User u) loginshell = tightenTargets $ check needchangeshell
(cmdProperty "chsh" ["--shell", loginshell, u])
`describe` (u ++ " has login shell " ++ loginshell)
where
needchangeshell = do
currshell <- userShell <$> getUserEntryForName u
return (currshell /= loginshell)
-- | Ensures that /etc/shells contains a shell.
shellEnabled :: FilePath -> Property DebianLike
shellEnabled loginshell = tightenTargets $
"/etc/shells" `File.containsLine` loginshell
|