Skip to content

Commit 845d1a4

Browse files
committed
add GetSeUserByName, fallback to failsafe context in GetDefaultContextWithLevel
Signed-off-by: Andrew LeFevre <[email protected]>
1 parent cf2eadf commit 845d1a4

File tree

4 files changed

+342
-23
lines changed

4 files changed

+342
-23
lines changed

go-selinux/selinux.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,19 @@ func DisableSecOpt() []string {
305305
return []string{"disable"}
306306
}
307307

308+
// GetSeUserByName retrieves the SELinux username and security level for a given
309+
// Linux username. The username and security level is based on the
310+
// /etc/selinux/{SELINUXTYPE}/seusers file.
311+
func GetSeUserByName(username string) (seUser string, level string, err error) {
312+
return getSeUserByName(username)
313+
}
314+
308315
// GetDefaultContextWithLevel gets a single context for the specified SELinux user
309316
// identity that is reachable from the specified scon context. The context is based
310317
// on the per-user /etc/selinux/{SELINUXTYPE}/contexts/users/<username> if it exists,
311318
// and falls back to the global /etc/selinux/{SELINUXTYPE}/contexts/default_contexts
312-
// file.
319+
// file and finally the global /etc/selinux/{SELINUXTYPE}/contexts/failsafe_context
320+
// file if no match can be found anywhere else.
313321
func GetDefaultContextWithLevel(user, level, scon string) (string, error) {
314322
return getDefaultContextWithLevel(user, level, scon)
315323
}

go-selinux/selinux_linux.go

Lines changed: 167 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const (
2727
selinuxDir = "/etc/selinux/"
2828
selinuxUsersDir = "contexts/users"
2929
defaultContexts = "contexts/default_contexts"
30+
failsafeContext = "contexts/failsafe_context"
3031
selinuxConfig = selinuxDir + "config"
3132
selinuxfsMount = "/sys/fs/selinux"
3233
selinuxTypeTag = "SELINUXTYPE"
@@ -57,6 +58,7 @@ type defaultSECtx struct {
5758
userRdr io.Reader
5859
verifier func(string) error
5960
defaultRdr io.Reader
61+
failsafeRdr io.Reader
6062
user, level, scon string
6163
}
6264

@@ -1181,6 +1183,117 @@ func dupSecOpt(src string) ([]string, error) {
11811183
return dup, nil
11821184
}
11831185

1186+
// checkGroup returns true if group's GID is in the list of GIDs gids.
1187+
func checkGroup(group string, gids []string, lookupGroup func(string) (*user.Group, error)) bool {
1188+
grp, err := lookupGroup(group)
1189+
if err != nil {
1190+
return false
1191+
}
1192+
1193+
for _, gid := range gids {
1194+
if grp.Gid == gid {
1195+
return true
1196+
}
1197+
}
1198+
return false
1199+
}
1200+
1201+
// getSeUserFromReader reads the seusers file: https://www.man7.org/linux/man-pages/man5/seusers.5.html
1202+
func getSeUserFromReader(username string, gids []string, r io.Reader, lookupGroup func(string) (*user.Group, error)) (seUser string, level string, err error) {
1203+
var defaultSeUser, defaultLevel string
1204+
var groupSeUser, groupLevel string
1205+
1206+
lineNum := -1
1207+
scanner := bufio.NewScanner(r)
1208+
for scanner.Scan() {
1209+
line := scanner.Text()
1210+
lineNum++
1211+
1212+
// remove any trailing comments, then extra whitespace
1213+
parts := strings.SplitN(line, "#", 2)
1214+
line = strings.TrimSpace(parts[0])
1215+
if line == "" {
1216+
continue
1217+
}
1218+
1219+
parts = strings.SplitN(line, ":", 3)
1220+
if len(parts) < 2 {
1221+
return "", "", fmt.Errorf("line %d: malformed line", lineNum)
1222+
}
1223+
userField := parts[0]
1224+
if userField == "" {
1225+
return "", "", fmt.Errorf("line %d: user_id or group_id is empty", lineNum)
1226+
}
1227+
seUserField := parts[1]
1228+
if seUserField == "" {
1229+
return "", "", fmt.Errorf("line %d: seuser_id is empty", lineNum)
1230+
}
1231+
var levelField string
1232+
// level is optional
1233+
if len(parts) > 2 {
1234+
levelField = parts[2]
1235+
}
1236+
1237+
// we found a match, return it
1238+
if userField == username {
1239+
return seUserField, levelField, nil
1240+
}
1241+
1242+
// if the first field starts with '%' it's a group, check if
1243+
// the user is a member of that group and set the group
1244+
// SELinux user and level if so
1245+
if userField[0] == '%' && groupSeUser == "" {
1246+
if checkGroup(userField[1:], gids, lookupGroup) {
1247+
groupSeUser = seUserField
1248+
groupLevel = levelField
1249+
}
1250+
} else if userField == "__default__" && defaultSeUser == "" {
1251+
defaultSeUser = seUserField
1252+
defaultLevel = levelField
1253+
}
1254+
}
1255+
if err := scanner.Err(); err != nil {
1256+
return "", "", fmt.Errorf("failed to read seusers file: %w", err)
1257+
}
1258+
1259+
if groupSeUser != "" {
1260+
return groupSeUser, groupLevel, nil
1261+
}
1262+
if defaultSeUser != "" {
1263+
return defaultSeUser, defaultLevel, nil
1264+
}
1265+
1266+
return "", "", fmt.Errorf("could not find SELinux user for %q login", username)
1267+
}
1268+
1269+
// getSeUserByName returns an SELinux user and MLS level that is
1270+
// mapped to a given Linux user.
1271+
func getSeUserByName(username string) (seUser string, level string, err error) {
1272+
seUsersConf := filepath.Join(policyRoot(), "seusers")
1273+
confFile, err := os.Open(seUsersConf)
1274+
if err != nil {
1275+
return "", "", fmt.Errorf("failed to open seusers file: %w", err)
1276+
}
1277+
defer confFile.Close()
1278+
1279+
usr, err := user.Lookup(username)
1280+
if err != nil {
1281+
return "", "", fmt.Errorf("failed to lookup user %q", username)
1282+
}
1283+
gids, err := usr.GroupIds()
1284+
if err != nil {
1285+
return "", "", fmt.Errorf("failed to find user %q's groups", username)
1286+
}
1287+
gids = append([]string{usr.Gid}, gids...)
1288+
1289+
seUser, level, err = getSeUserFromReader(username, gids, confFile, user.LookupGroup)
1290+
if err != nil {
1291+
return "", "", fmt.Errorf("failed to parse seusers file: %w", err)
1292+
}
1293+
1294+
return seUser, level, nil
1295+
}
1296+
11841297
// findUserInContext scans the reader for a valid SELinux context
11851298
// match that is verified with the verifier. Invalid contexts are
11861299
// skipped. It returns a matched context or an empty string if no
@@ -1238,6 +1351,33 @@ func findUserInContext(context Context, r io.Reader, verifier func(string) error
12381351
return "", nil
12391352
}
12401353

1354+
// getFailsafeContext returns the context in the failsafe_context file:
1355+
// https://www.man7.org/linux/man-pages/man5/failsafe_context.5.html
1356+
func getFailsafeContext(context Context, r io.Reader, verifier func(string) error) (string, error) {
1357+
conn := make([]byte, 256)
1358+
limReader := io.LimitReader(r, int64(len(conn)))
1359+
_, err := limReader.Read(conn)
1360+
if err != nil {
1361+
return "", fmt.Errorf("failed to read failsafe context: %w", err)
1362+
}
1363+
1364+
conn = bytes.TrimSpace(conn)
1365+
toConns := strings.SplitN(string(conn), ":", 4)
1366+
if len(toConns) != 3 {
1367+
return "", nil
1368+
}
1369+
1370+
context["role"] = toConns[0]
1371+
context["type"] = toConns[1]
1372+
1373+
outConn := context.get()
1374+
if err := verifier(outConn); err != nil {
1375+
return "", nil
1376+
}
1377+
1378+
return outConn, nil
1379+
}
1380+
12411381
func getDefaultContextFromReaders(c *defaultSECtx) (string, error) {
12421382
if c.verifier == nil {
12431383
return "", ErrVerifierNil
@@ -1254,7 +1394,7 @@ func getDefaultContextFromReaders(c *defaultSECtx) (string, error) {
12541394

12551395
conn, err := findUserInContext(context, c.userRdr, c.verifier)
12561396
if err != nil {
1257-
return "", err
1397+
return "", fmt.Errorf("failed to read %q's user context file: %w", c.user, err)
12581398
}
12591399

12601400
if conn != "" {
@@ -1263,7 +1403,16 @@ func getDefaultContextFromReaders(c *defaultSECtx) (string, error) {
12631403

12641404
conn, err = findUserInContext(context, c.defaultRdr, c.verifier)
12651405
if err != nil {
1266-
return "", err
1406+
return "", fmt.Errorf("failed to read default user context file: %w", err)
1407+
}
1408+
1409+
if conn != "" {
1410+
return conn, nil
1411+
}
1412+
1413+
conn, err = getFailsafeContext(context, c.failsafeRdr, c.verifier)
1414+
if err != nil {
1415+
return "", fmt.Errorf("failed to read failsafe_context: %w", err)
12671416
}
12681417

12691418
if conn != "" {
@@ -1277,24 +1426,32 @@ func getDefaultContextWithLevel(user, level, scon string) (string, error) {
12771426
userPath := filepath.Join(policyRoot(), selinuxUsersDir, user)
12781427
fu, err := os.Open(userPath)
12791428
if err != nil {
1280-
return "", err
1429+
return "", fmt.Errorf("failed to open %q's user context file: %w", user, err)
12811430
}
12821431
defer fu.Close()
12831432

12841433
defaultPath := filepath.Join(policyRoot(), defaultContexts)
12851434
fd, err := os.Open(defaultPath)
12861435
if err != nil {
1287-
return "", err
1436+
return "", fmt.Errorf("failed to open default user context file: %w", err)
12881437
}
12891438
defer fd.Close()
12901439

1440+
failsafePath := filepath.Join(policyRoot(), failsafeContext)
1441+
fs, err := os.Open(failsafePath)
1442+
if err != nil {
1443+
return "", fmt.Errorf("failed to open failsafe user context file: %w", err)
1444+
}
1445+
defer fs.Close()
1446+
12911447
c := defaultSECtx{
1292-
user: user,
1293-
level: level,
1294-
scon: scon,
1295-
userRdr: fu,
1296-
defaultRdr: fd,
1297-
verifier: securityCheckContext,
1448+
user: user,
1449+
level: level,
1450+
scon: scon,
1451+
userRdr: fu,
1452+
defaultRdr: fd,
1453+
failsafeRdr: fs,
1454+
verifier: securityCheckContext,
12981455
}
12991456

13001457
return getDefaultContextFromReaders(&c)

0 commit comments

Comments
 (0)