diff mbox

[3/5] fedfsd: Control access to ADMIN service

Message ID 20131218171753.7774.84023.stgit@seurat.1015granger.net
State Accepted
Headers show

Commit Message

Chuck Lever Dec. 18, 2013, 5:17 p.m. UTC
Currently fedfsd does not examine the credentials of incoming ADMIN
requests.  It responds to all comers, which is great for testing,
but makes fedfsd undeployable in any real environment.

To remedy this situation, provide support for authentication and
access control.  An access control list is kept in:

  /etc/fedfsd/access.conf

AUTH_NONE does not pass user credentials, but AUTH_SYS does.  We
introduce support for unpacking and checking AUTH_SYS credentials.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 Makefile.am                |    2 
 configure.ac               |    8 +
 doc/man/rpc.fedfsd.8       |   35 ++++
 src/fedfsd/Makefile.am     |    4 
 src/fedfsd/access.c        |  425 ++++++++++++++++++++++++++++++++++++++++++++
 src/fedfsd/fedfsd.h        |   15 ++
 src/fedfsd/main.c          |    3 
 src/fedfsd/svc.c           |   38 ++++
 sysconf/Makefile.am        |   29 +++
 sysconf/fedfsd/access.conf |   27 +++
 10 files changed, 582 insertions(+), 4 deletions(-)
 create mode 100644 src/fedfsd/access.c
 create mode 100644 sysconf/Makefile.am
 create mode 100644 sysconf/fedfsd/access.conf
diff mbox

Patch

diff --git a/Makefile.am b/Makefile.am
index b89de65..0462801 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -24,7 +24,7 @@ 
 ##
 
 AUTOMAKE_OPTIONS	= foreign
-SUBDIRS			= doc src contrib
+SUBDIRS			= doc src contrib sysconf
 dist_noinst_SCRIPTS	= autogen.sh
 EXTRA_DIST		= ChangeLog COPYING README INSTALL doxy.cfg .gitignore
 ACLOCAL_AMFLAGS		= -I m4
diff --git a/configure.ac b/configure.ac
index 3620b83..b692185 100644
--- a/configure.ac
+++ b/configure.ac
@@ -150,6 +150,11 @@  AC_CHECK_LIB([gssapi_krb5], [gss_acquire_cred],
 		 AC_DEFINE([HAVE_LIBGSSAPI_KRB5], [1],
 			   [Define if you have libgssapi_krb5])],
 		[AC_MSG_ERROR([libgssapi_krb5 not found.])])
+AC_CHECK_LIB([config], [config_lookup],
+		[AC_SUBST([LIBCONFIG], ["-lconfig"])
+		 AC_DEFINE([HAVE_LIBCONFIG], [1],
+			   [Define if you have libconfig])],
+		[AC_MSG_ERROR([libconfig not found.])])
 
 # Checks for header files.
 AC_CHECK_HEADERS([fcntl.h langinfo.h locale.h memory.h netdb.h netinet/in.h stdint.h stdlib.h string.h sys/socket.h syslog.h termios.h unistd.h wchar.h])
@@ -206,5 +211,6 @@  AC_CONFIG_FILES([Makefile
                  src/PyFedfs/Makefile
                  src/PyFedfs/domainroot/Makefile
                  src/PyFedfs/jumpstart/Makefile
-                 src/plug-ins/Makefile])
+                 src/plug-ins/Makefile
+                 sysconf/Makefile])
 AC_OUTPUT
diff --git a/doc/man/rpc.fedfsd.8 b/doc/man/rpc.fedfsd.8
index 538f659..668cfc1 100644
--- a/doc/man/rpc.fedfsd.8
+++ b/doc/man/rpc.fedfsd.8
@@ -106,6 +106,38 @@  Specifies the port number used for RPC listener sockets.
 If this option is not specified,
 .BR rpc.fedfsd (8)
 chooses a random ephemeral port for each listener socket.
+.SS Access control
+An Access Control List stored in
+.I /etc/fedfsd/access.conf
+manages whom
+.BR rpc.fedfsd (8)
+allows to perform ADMIN operations.
+The following access types are supported:
+.IP "\fBnone\fP"
+Enabling
+.B none
+allows anyone using
+.B AUTH_NONE
+security to perform ADMIN operations.
+.B none
+is for backwards compatibility only.
+It is not recommended for use in production deployments.
+.IP "\fBunix\fP"
+This setting specifies lists of users and groups who are allowed to use
+.B AUTH_SYS
+security to perform ADMIN operations.
+Though the
+.B unix
+setting
+provides more security than the
+.BR none
+setting,
+.B unix
+is not recommended for use on untrusted networks.
+.P
+See comments in
+.I /etc/fedfsd/access.conf
+for details on syntax of the Access Control List.
 .SH NOTES
 To create, resolve, or delete a junction, FedFS admin clients
 specify the pathname of that junction as an argument to the
@@ -136,6 +168,9 @@  database of NSDB connection parameters
 .TP
 .I @statedir@/nsdbcerts
 local directory that stores X.509 certificates for NSDBs
+.TP
+.I /etc/fedfsd/access.conf
+controls remote access to rpc.fedfsd
 .SH "SEE ALSO"
 .BR fedfs (7),
 .BR nfs (5)
diff --git a/src/fedfsd/Makefile.am b/src/fedfsd/Makefile.am
index 8ff35b9..a557bbd 100644
--- a/src/fedfsd/Makefile.am
+++ b/src/fedfsd/Makefile.am
@@ -26,14 +26,14 @@ 
 noinst_HEADERS		= fedfsd.h
 RPCPREFIX		= rpc.
 sbin_PROGRAMS		= fedfsd
-fedfsd_SOURCES		= listen.c main.c privilege.c svc.c
+fedfsd_SOURCES		= access.c listen.c main.c privilege.c svc.c
 fedfsd_LDADD		= $(top_builddir)/src/libadmin/libadmin.la \
 			  $(top_builddir)/src/libnsdb/libnsdb.la \
 			  $(top_builddir)/src/libjunction/libjunction.la \
 			  $(top_builddir)/src/libxlog/libxlog.la \
 			  $(LIBTIRPC) $(LIBLDAP) $(LIBLBER) $(LIBXML2) \
 			  $(LIBSQLITE3) $(LIBIDN) $(LIBUUID) $(LIBCAP) \
-			  $(LIBURIPARSER) $(LIBCRYPTO) $(LIBSSL)
+			  $(LIBURIPARSER) $(LIBCRYPTO) $(LIBSSL) $(LIBCONFIG)
 
 CLEANFILES		= cscope.in.out cscope.out cscope.po.out *~
 DISTCLEANFILES		= Makefile.in
diff --git a/src/fedfsd/access.c b/src/fedfsd/access.c
new file mode 100644
index 0000000..dd77bd1
--- /dev/null
+++ b/src/fedfsd/access.c
@@ -0,0 +1,425 @@ 
+/**
+ * @file src/fedfsd/access.c
+ * @brief fedfsd per-request authorization
+ */
+
+/*
+ * Copyright 2013 Oracle.  All rights reserved.
+ *
+ * This file is part of fedfs-utils.
+ *
+ * fedfs-utils is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2.0 as
+ * published by the Free Software Foundation.
+ *
+ * fedfs-utils is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License version 2.0 for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2.0 along with fedfs-utils.  If not, see:
+ *
+ *	http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <pwd.h>
+#include <grp.h>
+
+#include <linux/limits.h>
+#include <rpc/auth.h>
+#include <rpc/auth_unix.h>
+#include <libconfig.h>
+
+#include "fedfs.h"
+#include "nsdb.h"
+#include "fedfsd.h"
+#include "xlog.h"
+
+static char fedfsd_access_pathname[PATH_MAX + 1];
+static struct stat fedfsd_access_stat;
+static config_t fedfsd_acl;
+
+/**
+ * Predicate: Is AUTH_NONE access allowed?
+ *
+ * @return one if AUTH_NONE access is allowed, otherwise zero
+ */
+static int
+fedfsd_none_is_allowed(void)
+{
+	int value;
+
+	if (config_lookup_bool(&fedfsd_acl, "none", &value) == CONFIG_FALSE)
+		return 0;
+	if (value == 0)
+		return 0;
+	return 1;
+}
+
+/**
+ * Predicate: Is AUTH_UNIX access allowed?
+ *
+ * @return one if AUTH_UNIX access is allowed, otherwise zero
+ */
+static int
+fedfsd_unix_is_allowed(void)
+{
+	config_setting_t *setting;
+
+	setting = config_lookup(&fedfsd_acl, "unix.users");
+	if (setting != NULL)
+		if (config_setting_length(setting) > 0)
+			return 1;
+
+	setting = config_lookup(&fedfsd_acl, "unix.groups");
+	if (setting != NULL)
+		if (config_setting_length(setting) > 0)
+			return 1;
+
+	return 0;
+}
+
+/**
+ * Read and parse the access configuration file
+ *
+ * @return true if file was parsed, otherwise false
+ */
+static _Bool
+fedfsd_read_config(void)
+{
+	unsigned count;
+
+	if (stat(fedfsd_access_pathname, &fedfsd_access_stat) == -1) {
+		xlog(L_ERROR, "%s: Failed to stat %s: %m",
+			__func__, fedfsd_access_pathname);
+		return false;
+	}
+
+	config_init(&fedfsd_acl);
+
+	if (!config_read_file(&fedfsd_acl, fedfsd_access_pathname)) {
+		xlog(L_ERROR, "%s: %s:%d - %s", __func__,
+			config_error_file(&fedfsd_acl),
+			config_error_line(&fedfsd_acl),
+			config_error_text(&fedfsd_acl));
+		config_destroy(&fedfsd_acl);
+		return false;
+	}
+
+	count = fedfsd_none_is_allowed();
+	count += fedfsd_unix_is_allowed();
+	if (count == 0)
+		xlog(L_WARNING, "%s allows no access to the ADMIN service",
+			fedfsd_access_pathname);
+	return true;
+}
+
+/**
+ * Specify and read the access configuration file
+ *
+ * @param pathname NUL-terminated C string containing pathname of config file
+ * @return true if file was parsed, otherwise false
+ */
+_Bool
+fedfsd_read_access_config(const char *pathname)
+{
+	if (strlen(pathname) > sizeof(fedfsd_access_pathname)) {
+		xlog(L_ERROR, "Pathname of access config file is too long");
+		return false;
+	}
+	strcpy(fedfsd_access_pathname, pathname);
+
+	return fedfsd_read_config();
+}
+
+/**
+ * Check if access configuration file has been updated
+ *
+ * @return true if the in-memory config is up to date
+ */
+static _Bool
+fedfsd_reread_access_config(void)
+{
+	struct stat buf;
+
+	if (stat(fedfsd_access_pathname, &buf) == -1) {
+		xlog(L_ERROR, "%s: Failed to stat %s: %m",
+			__func__, fedfsd_access_pathname);
+		return false;
+	}
+	if (buf.st_mtime == fedfsd_access_stat.st_mtime) {
+		xlog(D_CALL, "%s: cached config still valid",
+			__func__);
+		return true;
+	}
+	xlog(D_CALL, "%s: re-reading config file", __func__);
+
+	config_destroy(&fedfsd_acl);
+	return fedfsd_read_config();
+}
+
+/**
+ * Decide if an AUTH_NONE access is authorized
+ *
+ * @return true if access is authorized
+ */
+_Bool
+fedfsd_auth_none(void)
+{
+	if (!fedfsd_reread_access_config())
+		return false;
+
+	if (fedfsd_none_is_allowed() == 0) {
+		xlog(D_CALL, "%s: caller not authorized", __func__);
+		return false;
+	}
+	xlog(D_CALL, "%s: caller authorized", __func__);
+	return true;
+}
+
+/**
+ * Decide if an AUTH_UNIX user matches list element
+ *
+ * @param users config setting containing a list
+ * @param i index of list element to check
+ * @param caller UID of requesting user
+ * @return true if "caller" matches the list element at "i"
+ */
+static _Bool
+fedfsd_check_unix_name(config_setting_t *users, int i, uid_t caller)
+{
+	struct passwd *pwd;
+	const char *name;
+
+	name = config_setting_get_string_elem(users, i);
+	if (name == NULL)
+		return false;
+
+	pwd = getpwnam(name);
+	if (pwd == NULL)
+		return false;
+
+	if (caller != pwd->pw_uid)
+		return false;
+
+	xlog(D_CALL, "%s: caller %s authorized",
+		__func__, name);
+	return true;
+}
+
+/**
+ * Decide if an AUTH_UNIX user matches list element
+ *
+ * @param users config setting containing a list
+ * @param i index of list element to check
+ * @param caller UID of requesting user
+ * @return true if "caller" matches the list element at "i"
+ */
+static _Bool
+fedfsd_check_unix_uid(config_setting_t *users, int i, uid_t caller)
+{
+	config_setting_t *uid;
+
+	uid = config_setting_get_elem(users, i);
+	if (uid == NULL)
+		return false;
+
+	if (config_setting_type(uid) != CONFIG_TYPE_INT)
+		return false;
+
+	if (caller != (uid_t)config_setting_get_int(uid))
+		return false;
+
+	xlog(D_CALL, "%s: caller %u authorized",
+		__func__, caller);
+	return true;
+}
+
+/**
+ * Decide if an AUTH_UNIX user is authorized
+ *
+ * @param users config setting containing a list
+ * @param caller UID of requesting user
+ * @return true if "caller" matches an element in "users"
+ */
+static _Bool
+fedfsd_check_unix_users(config_setting_t *users, uid_t caller)
+{
+	int i, count;
+
+	count = config_setting_length(users);
+	for (i = 0; i < count; i++) {
+		if (fedfsd_check_unix_name(users, i, caller))
+			return true;
+		if (fedfsd_check_unix_uid(users, i, caller))
+			return true;
+	}
+
+	xlog(D_CALL, "%s: caller %d not authorized",
+		__func__, caller);
+	return false;
+}
+
+/**
+ * Decide if an AUTH_UNIX user is authorized
+ *
+ * @param caller UID of requesting user
+ * @return true if access is authorized
+ */
+static _Bool
+fedfsd_auth_unix_user(uid_t caller)
+{
+	config_setting_t *users;
+
+	users = config_lookup(&fedfsd_acl, "unix.users");
+	if (users == NULL) {
+		xlog(D_CALL, "%s: caller not authorized", __func__);
+		return false;
+	}
+
+	return fedfsd_check_unix_users(users, caller);
+}
+
+/**
+ * Decide if an AUTH_UNIX group matches list element
+ *
+ * @param groups config setting containing a list
+ * @param i index of list element to check
+ * @param caller GID of requesting user
+ * @return true if "caller" matches the list element at "i"
+ */
+static _Bool
+fedfsd_check_unix_group(config_setting_t *groups, int i, uid_t caller)
+{
+	struct group *grp;
+	const char *name;
+
+	name = config_setting_get_string_elem(groups, i);
+	if (name == NULL)
+		return false;
+
+	grp = getgrnam(name);
+	if (grp == NULL)
+		return false;
+
+	if (caller != grp->gr_gid)
+		return false;
+
+	xlog(D_CALL, "%s: caller %s authorized",
+		__func__, name);
+	return true;
+}
+
+/**
+ * Decide if an AUTH_UNIX user matches list element
+ *
+ * @param groups config setting containing a list
+ * @param i index of list element to check
+ * @param caller GID of requesting user
+ * @return true if "caller" matches the list element at "i"
+ */
+static _Bool
+fedfsd_check_unix_gid(config_setting_t *groups, int i, gid_t caller)
+{
+	config_setting_t *gid;
+
+	gid = config_setting_get_elem(groups, i);
+	if (gid == NULL)
+		return false;
+
+	if (config_setting_type(gid) != CONFIG_TYPE_INT)
+		return false;
+
+	if (caller != (gid_t)config_setting_get_int(gid))
+		return false;
+
+	xlog(D_CALL, "%s: caller %u authorized",
+		__func__, caller);
+	return true;
+}
+
+/**
+ * Decide if an AUTH_UNIX group is authorized
+ *
+ * @param groups config setting containing a list
+ * @param caller GID of requesting user
+ * @return true if "caller" matches an element in "users"
+ */
+static _Bool
+fedfsd_check_unix_groups(config_setting_t *groups, gid_t caller)
+{
+	int i, count;
+
+	count = config_setting_length(groups);
+	for (i = 0; i < count; i++) {
+		if (fedfsd_check_unix_group(groups, i, caller))
+			return true;
+		if (fedfsd_check_unix_gid(groups, i, caller))
+			return true;
+	}
+	return false;
+}
+
+/**
+ * Decide if an AUTH_UNIX group is authorized
+ *
+ * @param gid primary GID of requesting user
+ * @param len count of items in "gids"
+ * @param gids array of secondary GIDs of requesting user
+ * @return true if access is authorized
+ */
+static _Bool
+fedfsd_auth_unix_group(gid_t gid, unsigned int len, gid_t *gids)
+{
+	config_setting_t *groups;
+	unsigned int i;
+
+	groups = config_lookup(&fedfsd_acl, "unix.groups");
+	if (groups == NULL) {
+		xlog(D_CALL, "%s: caller not authorized", __func__);
+		return false;
+	}
+
+	if (fedfsd_check_unix_groups(groups, gid))
+		return true;
+
+	for (i = 0; i < len; i++)
+		if (fedfsd_check_unix_groups(groups, gids[i]))
+			return true;
+
+	xlog(D_CALL, "%s: caller not authorized", __func__);
+	return false;
+}
+
+/**
+ * Decide if an AUTH_UNIX caller is authorized
+ *
+ * @param rqstp incoming RPC request
+ * @return true if caller is authorized to proceed
+ */
+_Bool
+fedfsd_auth_unix(struct svc_req *rqstp)
+{
+	struct authunix_parms *cred =
+			(struct authunix_parms *)rqstp->rq_clntcred;
+
+	xlog(D_CALL, "%s: uid=%d gid=%d",
+		__func__, cred->aup_uid, cred->aup_gid);
+
+	if (!fedfsd_reread_access_config())
+		return false;
+
+	if (fedfsd_auth_unix_user(cred->aup_uid))
+		return true;
+
+	return fedfsd_auth_unix_group(cred->aup_gid,
+					cred->aup_len, cred->aup_gids);
+}
diff --git a/src/fedfsd/fedfsd.h b/src/fedfsd/fedfsd.h
index f9ee5d2..c199818 100644
--- a/src/fedfsd/fedfsd.h
+++ b/src/fedfsd/fedfsd.h
@@ -26,12 +26,27 @@ 
 #ifndef _FEDFSD_H_
 #define _FEDFSD_H_
 
+#include <sys/types.h>
+
+#include <stdbool.h>
 #include <time.h>
 #include <ldap.h>
 #include <sqlite3.h>
 
+#include <rpc/svc.h>
+
 #include "fedfs_admin.h"
 
+#define FEDFSD_ACCESS_CONFIG	"/etc/fedfsd/access.conf"
+
+/*
+ * auth.c
+ */
+_Bool		fedfsd_read_access_config(const char *pathname);
+
+_Bool		fedfsd_auth_none(void);
+_Bool		fedfsd_auth_unix(struct svc_req *rqstp);
+
 /*
  * listen.c
  */
diff --git a/src/fedfsd/main.c b/src/fedfsd/main.c
index 6038a75..54fd91e 100644
--- a/src/fedfsd/main.c
+++ b/src/fedfsd/main.c
@@ -213,6 +213,9 @@  int main(int argc, char **argv)
 	if (!nsdb_init_database())
 		exit(EXIT_FAILURE);
 
+	if (!fedfsd_read_access_config(FEDFSD_ACCESS_CONFIG))
+		exit(EXIT_FAILURE);
+
 	if (!foreground) {
 		if (chdir(FEDFS_DEFAULT_STATEDIR) == -1) {
 			xlog(L_ERROR, "chdir: %m");
diff --git a/src/fedfsd/svc.c b/src/fedfsd/svc.c
index 218ef24..538def4 100644
--- a/src/fedfsd/svc.c
+++ b/src/fedfsd/svc.c
@@ -42,6 +42,7 @@ 
 
 #include <netinet/in.h>
 #include <uuid/uuid.h>
+
 #include <rpc/rpc.h>
 #include <rpc/svc.h>
 
@@ -52,6 +53,36 @@ 
 #include "xlog.h"
 
 /**
+ * Predicate: Is caller authorized?
+ *
+ * @param rqstp incoming RPC request
+ * @return true if caller is authorized to proceed
+ */
+static _Bool
+fedfsd_is_authorized(struct svc_req *rqstp)
+{
+	_Bool authorized;
+
+	if (rqstp->rq_proc == FEDFS_NULL)
+		return true;
+
+	authorized = false;
+	switch (rqstp->rq_cred.oa_flavor) {
+	case AUTH_NONE:
+		authorized = fedfsd_auth_none();
+		break;
+	case AUTH_SYS:
+		authorized = fedfsd_auth_unix(rqstp);
+		break;
+	default:
+		xlog(L_ERROR, "Procedure %d used unsupported security flavor",
+			rqstp->rq_proc);
+	}
+
+	return authorized;
+}
+
+/**
  * Report calling client's IP address via xlog()
  *
  * @param rqstp incoming RPC request
@@ -1318,6 +1349,13 @@  fedfsd_dispatch_1(struct svc_req *rqstp, SVCXPRT *xprt)
 	char addrbuf[INET6_ADDRSTRLEN];
 
 	fedfsd_caller(rqstp, addrbuf, sizeof(addrbuf));
+
+	if (!fedfsd_is_authorized(rqstp)) {
+		xlog(L_WARNING, "Caller from %s is unauthorized", addrbuf);
+		svcerr_auth(xprt, AUTH_REJECTEDCRED);
+		return;
+	}
+
 	switch (rqstp->rq_proc) {
 	case NULLPROC:
 		xlog(D_CALL, "%s: Received NULLPROC request from %s",
diff --git a/sysconf/Makefile.am b/sysconf/Makefile.am
new file mode 100644
index 0000000..31b339c
--- /dev/null
+++ b/sysconf/Makefile.am
@@ -0,0 +1,29 @@ 
+##
+## @file sysconf/Makefile.am
+## @brief Process this file with automake to produce sysconf/Makefile.in
+##
+
+##
+## Copyright 2013 Oracle.  All rights reserved.
+##
+## This file is part of fedfs-utils.
+##
+## fedfs-utils is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License version 2.0 as
+## published by the Free Software Foundation.
+##
+## fedfs-utils is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License version 2.0 for more details.
+##
+## You should have received a copy of the GNU General Public License
+## version 2.0 along with fedfs-utils.  If not, see:
+##
+##	http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
+##
+
+dist_sysconf_DATA	= fedfsd/access.conf
+
+CLEANFILES		= cscope.* *~
+DISTCLEANFILES		= Makefile.in
diff --git a/sysconf/fedfsd/access.conf b/sysconf/fedfsd/access.conf
new file mode 100644
index 0000000..7871f6f
--- /dev/null
+++ b/sysconf/fedfsd/access.conf
@@ -0,0 +1,27 @@ 
+##
+## /etc/fedfsd/access.conf
+##
+## Access control list for rpc.fedfsd
+##
+## Define the security flavors and users/principals authorized to
+## perform FedFS ADMIN operations on this server.
+##
+## One or more of the following must be uncommented to permit
+## access to the FedFS ADMIN service via rpc.fedfsd.
+##
+
+## Uncomment to authorize any request using AUTH_NONE security.
+## This setting is only for backwards compatibility and is not
+## recommended for use in production deployments.
+# none = true;
+
+## Uncomment and update these lists to authorize users and groups
+## permitted to perform ADMIN requests using AUTH_SYS security.
+## To enable UID or GID 0, specify a name whose UID or GID is zero.
+##
+## AUTH_SYS is not recommended for use on untrusted networks.
+# unix =
+# {
+#   users = ( "fedfs", "root", 99 );
+#   groups = ( "wheel", 55 );
+# };