diff options
Diffstat (limited to 'nixos')
23 files changed, 724 insertions, 93 deletions
diff --git a/nixos/doc/manual/configuration/declarative-packages.xml b/nixos/doc/manual/configuration/declarative-packages.xml index be9884fe9dce..c9acbefea60e 100644 --- a/nixos/doc/manual/configuration/declarative-packages.xml +++ b/nixos/doc/manual/configuration/declarative-packages.xml @@ -27,8 +27,13 @@ nixos.firefox firefox-23.0 Mozilla Firefox - the browser, reloaded <replaceable>...</replaceable> </screen> The first column in the output is the <emphasis>attribute name</emphasis>, - such as <literal>nixos.thunderbird</literal>. (The <literal>nixos</literal> - prefix allows distinguishing between different channels that you might have.) + such as <literal>nixos.thunderbird</literal>. + </para> + <para> + Note: the <literal>nixos</literal> prefix tells us that we want to get the + package from the <literal>nixos</literal> channel and works only in CLI tools. + + In declarative configuration use <literal>pkgs</literal> prefix (variable). </para> <para> diff --git a/nixos/doc/manual/release-notes/rl-1903.xml b/nixos/doc/manual/release-notes/rl-1903.xml index 7d40637df931..7c94f6e9473e 100644 --- a/nixos/doc/manual/release-notes/rl-1903.xml +++ b/nixos/doc/manual/release-notes/rl-1903.xml @@ -68,6 +68,17 @@ <xref linkend="sec-kubernetes"/> for details. </para> </listitem> + <listitem> + <para> + There is now a set of <option>confinement</option> options for + <option>systemd.services</option>, which allows to restrict services + into a <citerefentry> + <refentrytitle>chroot</refentrytitle> + <manvolnum>2</manvolnum> + </citerefentry>ed environment that only contains the store paths from + the runtime closure of the service. + </para> + </listitem> </itemizedlist> </section> @@ -516,6 +527,13 @@ Graylog</link> for details. </para> </listitem> + <listitem> + <para> + The option <literal>users.ldap.bind.password</literal> was renamed to <literal>users.ldap.bind.passwordFile</literal>, + and needs to be readable by the <literal>nslcd</literal> user. + Same applies to the new <literal>users.ldap.daemon.rootpwmodpwFile</literal> option. + </para> + </listitem> </itemizedlist> </section> diff --git a/nixos/modules/config/ldap.nix b/nixos/modules/config/ldap.nix index f65a3fc50d54..e008497a2a6e 100644 --- a/nixos/modules/config/ldap.nix +++ b/nixos/modules/config/ldap.nix @@ -27,25 +27,29 @@ let ''; }; - nslcdConfig = { - target = "nslcd.conf"; - source = writeText "nslcd.conf" '' - uid nslcd - gid nslcd - uri ${cfg.server} - base ${cfg.base} - timelimit ${toString cfg.timeLimit} - bind_timelimit ${toString cfg.bind.timeLimit} - ${optionalString (cfg.bind.distinguishedName != "") - "binddn ${cfg.bind.distinguishedName}" } - ${optionalString (cfg.daemon.rootpwmoddn != "") - "rootpwmoddn ${cfg.daemon.rootpwmoddn}" } - ${optionalString (cfg.daemon.extraConfig != "") cfg.daemon.extraConfig } - ''; - }; - - insertLdapPassword = !config.users.ldap.daemon.enable && - config.users.ldap.bind.distinguishedName != ""; + nslcdConfig = writeText "nslcd.conf" '' + uid nslcd + gid nslcd + uri ${cfg.server} + base ${cfg.base} + timelimit ${toString cfg.timeLimit} + bind_timelimit ${toString cfg.bind.timeLimit} + ${optionalString (cfg.bind.distinguishedName != "") + "binddn ${cfg.bind.distinguishedName}" } + ${optionalString (cfg.daemon.rootpwmoddn != "") + "rootpwmoddn ${cfg.daemon.rootpwmoddn}" } + ${optionalString (cfg.daemon.extraConfig != "") cfg.daemon.extraConfig } + ''; + + # nslcd normally reads configuration from /etc/nslcd.conf. + # this file might contain secrets. We append those at runtime, + # so redirect its location to something more temporary. + nslcdWrapped = runCommandNoCC "nslcd-wrapped" { nativeBuildInputs = [ makeWrapper ]; } '' + mkdir -p $out/bin + makeWrapper ${nss_pam_ldapd}/sbin/nslcd $out/bin/nslcd \ + --set LD_PRELOAD "${pkgs.libredirect}/lib/libredirect.so" \ + --set NIX_REDIRECTS "/etc/nslcd.conf=/run/nslcd/nslcd.conf" + ''; in @@ -139,13 +143,13 @@ in ''; }; - rootpwmodpw = mkOption { + rootpwmodpwFile = mkOption { default = ""; example = "/run/keys/nslcd.rootpwmodpw"; type = types.str; description = '' - The path to a file containing the credentials with which - to bind to the LDAP server if the root user tries to change a user's password + The path to a file containing the credentials with which to bind to + the LDAP server if the root user tries to change a user's password. ''; }; }; @@ -161,7 +165,7 @@ in ''; }; - password = mkOption { + passwordFile = mkOption { default = "/etc/ldap/bind.password"; type = types.str; description = '' @@ -220,14 +224,14 @@ in config = mkIf cfg.enable { - environment.etc = if cfg.daemon.enable then [nslcdConfig] else [ldapConfig]; + environment.etc = optional (!cfg.daemon.enable) ldapConfig; - system.activationScripts = mkIf insertLdapPassword { + system.activationScripts = mkIf (!cfg.daemon.enable) { ldap = stringAfter [ "etc" "groups" "users" ] '' - if test -f "${cfg.bind.password}" ; then + if test -f "${cfg.bind.passwordFile}" ; then umask 0077 conf="$(mktemp)" - printf 'bindpw %s\n' "$(cat ${cfg.bind.password})" | + printf 'bindpw %s\n' "$(cat ${cfg.bind.passwordFile})" | cat ${ldapConfig.source} - >"$conf" mv -fT "$conf" /etc/ldap.conf fi @@ -251,7 +255,6 @@ in }; systemd.services = mkIf cfg.daemon.enable { - nslcd = { wantedBy = [ "multi-user.target" ]; @@ -259,32 +262,32 @@ in umask 0077 conf="$(mktemp)" { - cat ${nslcdConfig.source} - test -z '${cfg.bind.distinguishedName}' -o ! -f '${cfg.bind.password}' || - printf 'bindpw %s\n' "$(cat '${cfg.bind.password}')" - test -z '${cfg.daemon.rootpwmoddn}' -o ! -f '${cfg.daemon.rootpwmodpw}' || - printf 'rootpwmodpw %s\n' "$(cat '${cfg.daemon.rootpwmodpw}')" + cat ${nslcdConfig} + test -z '${cfg.bind.distinguishedName}' -o ! -f '${cfg.bind.passwordFile}' || + printf 'bindpw %s\n' "$(cat '${cfg.bind.passwordFile}')" + test -z '${cfg.daemon.rootpwmoddn}' -o ! -f '${cfg.daemon.rootpwmodpwFile}' || + printf 'rootpwmodpw %s\n' "$(cat '${cfg.daemon.rootpwmodpwFile}')" } >"$conf" - mv -fT "$conf" /etc/nslcd.conf + mv -fT "$conf" /run/nslcd/nslcd.conf ''; - - # NOTE: because one cannot pass a custom config path to `nslcd` - # (which is only able to use `/etc/nslcd.conf`) - # changes in `nslcdConfig` won't change `serviceConfig`, - # and thus won't restart `nslcd`. - # Therefore `restartTriggers` is used on `/etc/nslcd.conf`. - restartTriggers = [ nslcdConfig.source ]; + restartTriggers = [ "/run/nslcd/nslcd.conf" ]; serviceConfig = { - ExecStart = "${nss_pam_ldapd}/sbin/nslcd"; + ExecStart = "${nslcdWrapped}/bin/nslcd"; Type = "forking"; - PIDFile = "/run/nslcd/nslcd.pid"; Restart = "always"; + User = "nslcd"; + Group = "nslcd"; RuntimeDirectory = [ "nslcd" ]; + PIDFile = "/run/nslcd/nslcd.pid"; }; }; }; }; + + imports = + [ (mkRenamedOptionModule [ "users" "ldap" "bind" "password"] [ "users" "ldap" "bind" "passwordFile"]) + ]; } diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix index d71e06202e30..12b8d85edf3b 100644 --- a/nixos/modules/installer/cd-dvd/iso-image.nix +++ b/nixos/modules/installer/cd-dvd/iso-image.nix @@ -88,7 +88,7 @@ let # result in incorrect boot entries. baseIsolinuxCfg = '' - SERIAL 0 38400 + SERIAL 0 115200 TIMEOUT ${builtins.toString syslinuxTimeout} UI vesamenu.c32 MENU TITLE NixOS @@ -338,8 +338,10 @@ let efiImg = pkgs.runCommand "efi-image_eltorito" { buildInputs = [ pkgs.mtools pkgs.libfaketime ]; } # Be careful about determinism: du --apparent-size, - # dates (cp -p, touch, mcopy -m, faketime for label), IDs (mkfs.vfat -i) + # dates (cp -p, touch, mcopy -m, faketime for label), IDs (mkfs.vfat -i), + # mcopy's write order (-s uses `readdir` order) '' + # Prepare the ./EFI and ./boot directories mkdir ./contents && cd ./contents cp -rp "${efiDir}"/EFI . mkdir ./boot @@ -347,6 +349,7 @@ let "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}" ./boot/ touch --date=@0 ./EFI ./boot + # Prepare the image file usage_size=$(du -sb --apparent-size . | tr -cd '[:digit:]') # Make the image 110% as big as the files need to make up for FAT overhead image_size=$(( ($usage_size * 110) / 100 )) @@ -356,8 +359,16 @@ let echo "Usage size: $usage_size" echo "Image size: $image_size" truncate --size=$image_size "$out" + + # Make the filesystem ${pkgs.libfaketime}/bin/faketime "2000-01-01 00:00:00" ${pkgs.dosfstools}/sbin/mkfs.vfat -i 12345678 -n EFIBOOT "$out" - mcopy -psvm -i "$out" ./EFI ./boot :: + + # Copy the files + # Note: we can't use mcopy's recursive copying as it uses `readdir` order. + # So just copy file-after-file + find ./EFI ./boot -type f -print0 | sort -z | \ + xargs -0I '{}' mcopy -pvm -i "$out" '{}' :: + # Verify the FAT partition. ${pkgs.dosfstools}/sbin/fsck.vfat -vn "$out" ''; # */ diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index dc571602581b..374e39f553fa 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -172,6 +172,7 @@ ./security/rtkit.nix ./security/wrappers/default.nix ./security/sudo.nix + ./security/systemd-confinement.nix ./services/admin/oxidized.nix ./services/admin/salt/master.nix ./services/admin/salt/minion.nix @@ -349,6 +350,7 @@ ./services/mail/exim.nix ./services/mail/freepops.nix ./services/mail/mail.nix + ./services/mail/mailcatcher.nix ./services/mail/mailhog.nix ./services/mail/mlmmj.nix ./services/mail/offlineimap.nix diff --git a/nixos/modules/security/duosec.nix b/nixos/modules/security/duosec.nix index 14bf118f2d84..997328ad9e6a 100644 --- a/nixos/modules/security/duosec.nix +++ b/nixos/modules/security/duosec.nix @@ -76,7 +76,7 @@ in }; failmode = mkOption { - type = types.enum [ "safe" "enum" ]; + type = types.enum [ "safe" "secure" ]; default = "safe"; description = '' On service or configuration errors that prevent Duo diff --git a/nixos/modules/security/systemd-confinement.nix b/nixos/modules/security/systemd-confinement.nix new file mode 100644 index 000000000000..cd4eb81dbe19 --- /dev/null +++ b/nixos/modules/security/systemd-confinement.nix @@ -0,0 +1,199 @@ +{ config, pkgs, lib, ... }: + +let + toplevelConfig = config; + inherit (lib) types; + inherit (import ../system/boot/systemd-lib.nix { + inherit config pkgs lib; + }) mkPathSafeName; +in { + options.systemd.services = lib.mkOption { + type = types.attrsOf (types.submodule ({ name, config, ... }: { + options.confinement.enable = lib.mkOption { + type = types.bool; + default = false; + description = '' + If set, all the required runtime store paths for this service are + bind-mounted into a <literal>tmpfs</literal>-based <citerefentry> + <refentrytitle>chroot</refentrytitle> + <manvolnum>2</manvolnum> + </citerefentry>. + ''; + }; + + options.confinement.fullUnit = lib.mkOption { + type = types.bool; + default = false; + description = '' + Whether to include the full closure of the systemd unit file into the + chroot, instead of just the dependencies for the executables. + + <warning><para>While it may be tempting to just enable this option to + make things work quickly, please be aware that this might add paths + to the closure of the chroot that you didn't anticipate. It's better + to use <option>confinement.packages</option> to <emphasis + role="strong">explicitly</emphasis> add additional store paths to the + chroot.</para></warning> + ''; + }; + + options.confinement.packages = lib.mkOption { + type = types.listOf (types.either types.str types.package); + default = []; + description = let + mkScOption = optName: "<option>serviceConfig.${optName}</option>"; + in '' + Additional packages or strings with context to add to the closure of + the chroot. By default, this includes all the packages from the + ${lib.concatMapStringsSep ", " mkScOption [ + "ExecReload" "ExecStartPost" "ExecStartPre" "ExecStop" + "ExecStopPost" + ]} and ${mkScOption "ExecStart"} options. If you want to have all the + dependencies of this systemd unit, you can use + <option>confinement.fullUnit</option>. + + <note><para>The store paths listed in <option>path</option> are + <emphasis role="strong">not</emphasis> included in the closure as + well as paths from other options except those listed + above.</para></note> + ''; + }; + + options.confinement.binSh = lib.mkOption { + type = types.nullOr types.path; + default = toplevelConfig.environment.binsh; + defaultText = "config.environment.binsh"; + example = lib.literalExample "\${pkgs.dash}/bin/dash"; + description = '' + The program to make available as <filename>/bin/sh</filename> inside + the chroot. If this is set to <literal>null</literal>, no + <filename>/bin/sh</filename> is provided at all. + + This is useful for some applications, which for example use the + <citerefentry> + <refentrytitle>system</refentrytitle> + <manvolnum>3</manvolnum> + </citerefentry> library function to execute commands. + ''; + }; + + options.confinement.mode = lib.mkOption { + type = types.enum [ "full-apivfs" "chroot-only" ]; + default = "full-apivfs"; + description = '' + The value <literal>full-apivfs</literal> (the default) sets up + private <filename class="directory">/dev</filename>, <filename + class="directory">/proc</filename>, <filename + class="directory">/sys</filename> and <filename + class="directory">/tmp</filename> file systems in a separate user + name space. + + If this is set to <literal>chroot-only</literal>, only the file + system name space is set up along with the call to <citerefentry> + <refentrytitle>chroot</refentrytitle> + <manvolnum>2</manvolnum> + </citerefentry>. + + <note><para>This doesn't cover network namespaces and is solely for + file system level isolation.</para></note> + ''; + }; + + config = let + rootName = "${mkPathSafeName name}-chroot"; + inherit (config.confinement) binSh fullUnit; + wantsAPIVFS = lib.mkDefault (config.confinement.mode == "full-apivfs"); + in lib.mkIf config.confinement.enable { + serviceConfig = { + RootDirectory = pkgs.runCommand rootName {} "mkdir \"$out\""; + TemporaryFileSystem = "/"; + PrivateMounts = lib.mkDefault true; + + # https://github.com/NixOS/nixpkgs/issues/14645 is a future attempt + # to change some of these to default to true. + # + # If we run in chroot-only mode, having something like PrivateDevices + # set to true by default will mount /dev within the chroot, whereas + # with "chroot-only" it's expected that there are no /dev, /proc and + # /sys file systems available. + # + # However, if this suddenly becomes true, the attack surface will + # increase, so let's explicitly set these options to true/false + # depending on the mode. + MountAPIVFS = wantsAPIVFS; + PrivateDevices = wantsAPIVFS; + PrivateTmp = wantsAPIVFS; + PrivateUsers = wantsAPIVFS; + ProtectControlGroups = wantsAPIVFS; + ProtectKernelModules = wantsAPIVFS; + ProtectKernelTunables = wantsAPIVFS; + }; + confinement.packages = let + execOpts = [ + "ExecReload" "ExecStart" "ExecStartPost" "ExecStartPre" "ExecStop" + "ExecStopPost" + ]; + execPkgs = lib.concatMap (opt: let + isSet = config.serviceConfig ? ${opt}; + in lib.optional isSet config.serviceConfig.${opt}) execOpts; + unitAttrs = toplevelConfig.systemd.units."${name}.service"; + allPkgs = lib.singleton (builtins.toJSON unitAttrs); + unitPkgs = if fullUnit then allPkgs else execPkgs; + in unitPkgs ++ lib.optional (binSh != null) binSh; + }; + })); + }; + + config.assertions = lib.concatLists (lib.mapAttrsToList (name: cfg: let + whatOpt = optName: "The 'serviceConfig' option '${optName}' for" + + " service '${name}' is enabled in conjunction with" + + " 'confinement.enable'"; + in lib.optionals cfg.confinement.enable [ + { assertion = !cfg.serviceConfig.RootDirectoryStartOnly or false; + message = "${whatOpt "RootDirectoryStartOnly"}, but right now systemd" + + " doesn't support restricting bind-mounts to 'ExecStart'." + + " Please either define a separate service or find a way to run" + + " commands other than ExecStart within the chroot."; + } + { assertion = !cfg.serviceConfig.DynamicUser or false; + message = "${whatOpt "DynamicUser"}. Please create a dedicated user via" + + " the 'users.users' option instead as this combination is" + + " currently not supported."; + } + ]) config.systemd.services); + + config.systemd.packages = lib.concatLists (lib.mapAttrsToList (name: cfg: let + rootPaths = let + contents = lib.concatStringsSep "\n" cfg.confinement.packages; + in pkgs.writeText "${mkPathSafeName name}-string-contexts.txt" contents; + + chrootPaths = pkgs.runCommand "${mkPathSafeName name}-chroot-paths" { + closureInfo = pkgs.closureInfo { inherit rootPaths; }; + serviceName = "${name}.service"; + excludedPath = rootPaths; + } '' + mkdir -p "$out/lib/systemd/system" + serviceFile="$out/lib/systemd/system/$serviceName" + + echo '[Service]' > "$serviceFile" + + # /bin/sh is special here, because the option value could contain a + # symlink and we need to properly resolve it. + ${lib.optionalString (cfg.confinement.binSh != null) '' + binsh=${lib.escapeShellArg cfg.confinement.binSh} + realprog="$(readlink -e "$binsh")" + echo "BindReadOnlyPaths=$realprog:/bin/sh" >> "$serviceFile" + ''} + + while read storePath; do + if [ -L "$storePath" ]; then + # Currently, systemd can't cope with symlinks in Bind(ReadOnly)Paths, + # so let's just bind-mount the target to that location. + echo "BindReadOnlyPaths=$(readlink -e "$storePath"):$storePath" + elif [ "$storePath" != "$excludedPath" ]; then + echo "BindReadOnlyPaths=$storePath" + fi + done < "$closureInfo/store-paths" >> "$serviceFile" + ''; + in lib.optional cfg.confinement.enable chrootPaths) config.systemd.services); +} diff --git a/nixos/modules/services/databases/mysql.nix b/nixos/modules/services/databases/mysql.nix index 467feb09b3a3..89291d4438ff 100644 --- a/nixos/modules/services/databases/mysql.nix +++ b/nixos/modules/services/databases/mysql.nix @@ -103,6 +103,24 @@ in }; initialDatabases = mkOption { + type = types.listOf (types.submodule { + options = { + name = mkOption { + type = types.str; + description = '' + The name of the database to create. + ''; + }; + schema = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + The initial schema of the database; if null (the default), + an empty database is created. + ''; + }; + }; + }); default = []; description = '' List of database names and their initial schemas that should be used to create databases on the first startup @@ -115,11 +133,13 @@ in }; initialScript = mkOption { + type = types.nullOr types.lines; default = null; description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database"; }; ensureDatabases = mkOption { + type = types.listOf types.str; default = []; description = '' Ensures that the specified databases exist. @@ -134,6 +154,38 @@ in }; ensureUsers = mkOption { + type = types.listOf (types.submodule { + options = { + name = mkOption { + type = types.str; + description = '' + Name of the user to ensure. + ''; + }; + ensurePermissions = mkOption { + type = types.attrsOf types.str; + default = {}; + description = '' + Permissions to ensure for the user, specified as attribute set. + The attribute names specify the database and tables to grant the permissions for, + separated by a dot. You may use wildcards here. + The attribute values specfiy the permissions to grant. + You may specify one or multiple comma-separated SQL privileges here. + + For more information on how to specify the target + and on which privileges exist, see the + <link xlink:href="https://mariadb.com/kb/en/library/grant/">GRANT syntax</link>. + The attributes are used as <code>GRANT ''${attrName} ON ''${attrValue}</code>. + ''; + example = literalExample '' + { + "database.*" = "ALL PRIVILEGES"; + "*.*" = "SELECT, LOCK TABLES"; + } + ''; + }; + }; + }); default = []; description = '' Ensures that the specified users exist and have at least the ensured permissions. @@ -143,20 +195,22 @@ in option is changed. This means that users created and permissions assigned once through this option or otherwise have to be removed manually. ''; - example = literalExample ''[ - { - name = "nextcloud"; - ensurePermissions = { - "nextcloud.*" = "ALL PRIVILEGES"; - }; - } - { - name = "backup"; - ensurePermissions = { - "*.*" = "SELECT, LOCK TABLES"; - }; - } - ]''; + example = literalExample '' + [ + { + name = "nextcloud"; + ensurePermissions = { + "nextcloud.*" = "ALL PRIVILEGES"; + }; + } + { + name = "backup"; + ensurePermissions = { + "*.*" = "SELECT, LOCK TABLES"; + }; + } + ] + ''; }; # FIXME: remove this option; it's a really bad idea. diff --git a/nixos/modules/services/mail/mailcatcher.nix b/nixos/modules/services/mail/mailcatcher.nix new file mode 100644 index 000000000000..2c6aadadce9d --- /dev/null +++ b/nixos/modules/services/mail/mailcatcher.nix @@ -0,0 +1,60 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.services.mailcatcher; + + inherit (lib) mkEnableOption mkIf mkOption types; +in +{ + # interface + + options = { + + services.mailcatcher = { + enable = mkEnableOption "Enable MailCatcher."; + + http.ip = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "The ip address of the http server."; + }; + + http.port = mkOption { + type = types.port; + default = 1080; + description = "The port address of the http server."; + }; + + smtp.ip = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "The ip address of the smtp server."; + }; + + smtp.port = mkOption { + type = types.port; + default = 1025; + description = "The port address of the smtp server."; + }; + }; + + }; + + # implementation + + config = mkIf cfg.enable { + environment.systemPackages = [ pkgs.mailcatcher ]; + + systemd.services.mailcatcher = { + description = "MailCatcher Service"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + DynamicUser = true; + Restart = "always"; + ExecStart = "${pkgs.mailcatcher}/bin/mailcatcher --foreground --no-quit --http-ip ${cfg.http.ip} --http-port ${toString cfg.http.port} --smtp-ip ${cfg.smtp.ip} --smtp-port ${toString cfg.smtp.port}"; + }; + }; + }; +} diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix index baa1c855c116..71277b48ecd9 100644 --- a/nixos/modules/services/misc/gitlab.nix +++ b/nixos/modules/services/misc/gitlab.nix @@ -160,6 +160,22 @@ let ''; }; + gitlab-rails = pkgs.stdenv.mkDerivation rec { + name = "gitlab-rails"; + buildInputs = [ pkgs.makeWrapper ]; + dontBuild = true; + unpackPhase = ":"; + installPhase = '' + mkdir -p $out/bin + makeWrapper ${cfg.packages.gitlab.rubyEnv}/bin/rails $out/bin/gitlab-rails \ + ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \ + --set PATH '${lib.makeBinPath [ pkgs.nodejs pkgs.gzip pkgs.git pkgs.gnutar config.services.postgresql.package pkgs.coreutils pkgs.procps ]}:$PATH' \ + --run 'cd ${cfg.packages.gitlab}/share/gitlab' + ''; + }; + + extraGitlabRb = pkgs.writeText "extra-gitlab.rb" cfg.extraGitlabRb; + smtpSettings = pkgs.writeText "gitlab-smtp-settings.rb" '' if Rails.env.production? Rails.application.config.action_mailer.delivery_method = :smtp @@ -266,6 +282,26 @@ in { description = "Extra configuration in config/database.yml."; }; + extraGitlabRb = mkOption { + type = types.str; + default = ""; + example = '' + if Rails.env.production? + Rails.application.config.action_mailer.delivery_method = :sendmail + ActionMailer::Base.delivery_method = :sendmail + ActionMailer::Base.sendmail_settings = { + location: "/run/wrappers/bin/sendmail", + arguments: "-i -t" + } + end + ''; + description = '' + Extra configuration to be placed in config/extra-gitlab.rb. This can + be used to add configuration not otherwise exposed through this module's + options. + ''; + }; + host = mkOption { type = types.str; default = config.networking.hostName; @@ -439,7 +475,7 @@ in { config = mkIf cfg.enable { - environment.systemPackages = [ pkgs.git gitlab-rake cfg.packages.gitlab-shell ]; + environment.systemPackages = [ pkgs.git gitlab-rake gitlab-rails cfg.packages.gitlab-shell ]; # Redis is required for the sidekiq queue runner. services.redis.enable = mkDefault true; @@ -512,6 +548,7 @@ in { wantedBy = [ "multi-user.target" ]; path = with pkgs; [ openssh + procps # See https://gitlab.com/gitlab-org/gitaly/issues/1562 gitAndTools.git cfg.packages.gitaly.rubyEnv cfg.packages.gitaly.rubyEnv.wrappedRuby @@ -586,6 +623,7 @@ in { [ -L /run/gitlab/uploads ] || ln -sf ${cfg.statePath}/uploads /run/gitlab/uploads cp ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION cp -rf ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config + ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb ${optionalString cfg.smtp.enable '' ln -sf ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb ''} diff --git a/nixos/modules/services/misc/plex.nix b/nixos/modules/services/misc/plex.nix index b06c1c4bbc68..fce9b29011f1 100644 --- a/nixos/modules/services/misc/plex.nix +++ b/nixos/modules/services/misc/plex.nix @@ -146,7 +146,7 @@ in PLEX_MEDIA_SERVER_MAX_PLUGIN_PROCS="6"; PLEX_MEDIA_SERVER_TMPDIR="/tmp"; PLEX_MEDIA_SERVER_USE_SYSLOG="true"; - LD_LIBRARY_PATH="/run/opengl-driver/lib:${cfg.package}/usr/lib/plexmediaserver"; + LD_LIBRARY_PATH="/run/opengl-driver/lib:${cfg.package}/usr/lib/plexmediaserver/lib"; LC_ALL="en_US.UTF-8"; LANG="en_US.UTF-8"; }; diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix index aba64e4f60ff..4ea891262e56 100644 --- a/nixos/modules/services/networking/firewall.nix +++ b/nixos/modules/services/networking/firewall.nix @@ -261,10 +261,14 @@ let fi ''; + canonicalizePortList = + ports: lib.unique (builtins.sort builtins.lessThan ports); + commonOptions = { allowedTCPPorts = mkOption { - type = types.listOf types.int; + type = types.listOf types.port; default = [ ]; + apply = canonicalizePortList; example = [ 22 80 ]; description = '' @@ -274,7 +278,7 @@ let }; allowedTCPPortRanges = mkOption { - type = types.listOf (types.attrsOf types.int); + type = types.listOf (types.attrsOf types.port); default = [ ]; example = [ { from = 8999; to = 9003; } ]; description = @@ -285,8 +289,9 @@ let }; allowedUDPPorts = mkOption { - type = types.listOf types.int; + type = types.listOf types.port; default = [ ]; + apply = canonicalizePortList; example = [ 53 ]; description = '' @@ -295,7 +300,7 @@ let }; allowedUDPPortRanges = mkOption { - type = types.listOf (types.attrsOf types.int); + type = types.listOf (types.attrsOf types.port); default = [ ]; example = [ { from = 60000; to = 61000; } ]; description = diff --git a/nixos/modules/services/scheduling/cron.nix b/nixos/modules/services/scheduling/cron.nix index 6f6977b38a1d..3bc31832946b 100644 --- a/nixos/modules/services/scheduling/cron.nix +++ b/nixos/modules/services/scheduling/cron.nix @@ -64,8 +64,8 @@ in sendmail. See <option>security.wrappers</option> If neither /var/cron/cron.deny nor /var/cron/cron.allow exist only root - will is allowed to have its own crontab file. The /var/cron/cron.deny file - is created automatically for you. So every user can use a crontab. + is allowed to have its own crontab file. The /var/cron/cron.deny file + is created automatically for you, so every user can use a crontab. Many nixos modules set systemCronJobs, so if you decide to disable vixie cron and enable another cron daemon, you may want it to get its system crontab diff --git a/nixos/modules/system/boot/systemd-lib.nix b/nixos/modules/system/boot/systemd-lib.nix index 68a40377ee13..28ad4f121bbe 100644 --- a/nixos/modules/system/boot/systemd-lib.nix +++ b/nixos/modules/system/boot/systemd-lib.nix @@ -9,12 +9,11 @@ in rec { shellEscape = s: (replaceChars [ "\\" ] [ "\\\\" ] s); + mkPathSafeName = lib.replaceChars ["@" ":" "\\" "[" "]"] ["-" "-" "-" "" ""]; + makeUnit = name: unit: - let - pathSafeName = lib.replaceChars ["@" ":" "\\" "[" "]"] ["-" "-" "-" "" ""] name; - in if unit.enable then - pkgs.runCommand "unit-${pathSafeName}" + pkgs.runCommand "unit-${mkPathSafeName name}" { preferLocalBuild = true; allowSubstitutes = false; inherit (unit) text; @@ -24,7 +23,7 @@ in rec { echo -n "$text" > $out/${shellEscape name} '' else - pkgs.runCommand "unit-${pathSafeName}-disabled" + pkgs.runCommand "unit-${mkPathSafeName name}-disabled" { preferLocalBuild = true; allowSubstitutes = false; } diff --git a/nixos/modules/virtualisation/docker-containers.nix b/nixos/modules/virtualisation/docker-containers.nix index 7cf871cc3bac..c4e47bfa477c 100644 --- a/nixos/modules/virtualisation/docker-containers.nix +++ b/nixos/modules/virtualisation/docker-containers.nix @@ -222,7 +222,7 @@ in { description = "Docker containers to run as systemd services."; }; - config = mkIf (cfg != []) { + config = mkIf (cfg != {}) { systemd.services = mapAttrs' (n: v: nameValuePair "docker-${n}" (mkService n v)) cfg; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index a5acf78a8839..395dc22f9682 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -131,6 +131,7 @@ in #lightdm = handleTest ./lightdm.nix {}; login = handleTest ./login.nix {}; #logstash = handleTest ./logstash.nix {}; + mailcatcher = handleTest ./mailcatcher.nix {}; mathics = handleTest ./mathics.nix {}; matrix-synapse = handleTest ./matrix-synapse.nix {}; memcached = handleTest ./memcached.nix {}; @@ -220,6 +221,7 @@ in switchTest = handleTest ./switch-test.nix {}; syncthing-relay = handleTest ./syncthing-relay.nix {}; systemd = handleTest ./systemd.nix {}; + systemd-confinement = handleTest ./systemd-confinement.nix {}; taskserver = handleTest ./taskserver.nix {}; telegraf = handleTest ./telegraf.nix {}; tomcat = handleTest ./tomcat.nix {}; diff --git a/nixos/tests/docker-tools.nix b/nixos/tests/docker-tools.nix index 399e4d4e428f..502b537ed68b 100644 --- a/nixos/tests/docker-tools.nix +++ b/nixos/tests/docker-tools.nix @@ -34,8 +34,8 @@ import ./make-test.nix ({ pkgs, ... }: { # To test the pullImage tool $docker->succeed("docker load --input='${pkgs.dockerTools.examples.nixFromDockerHub}'"); - $docker->succeed("docker run --rm nixos/nix:2.2.1 nix-store --version"); - $docker->succeed("docker rmi nixos/nix:2.2.1"); + $docker->succeed("docker run --rm nix:2.2.1 nix-store --version"); + $docker->succeed("docker rmi nix:2.2.1"); # To test runAsRoot and entry point $docker->succeed("docker load --input='${pkgs.dockerTools.examples.nginx}'"); diff --git a/nixos/tests/ldap.nix b/nixos/tests/ldap.nix index b3fd42e75886..fe859876ed25 100644 --- a/nixos/tests/ldap.nix +++ b/nixos/tests/ldap.nix @@ -28,20 +28,19 @@ let users.ldap.daemon = { enable = useDaemon; rootpwmoddn = "cn=admin,${dbSuffix}"; - rootpwmodpw = "/etc/nslcd.rootpwmodpw"; + rootpwmodpwFile = "/etc/nslcd.rootpwmodpw"; }; - # NOTE: password stored in clear in Nix's store, but this is a test. - environment.etc."nslcd.rootpwmodpw".source = pkgs.writeText "rootpwmodpw" dbAdminPwd; users.ldap.loginPam = true; users.ldap.nsswitch = true; users.ldap.server = "ldap://server"; users.ldap.base = "ou=posix,${dbSuffix}"; users.ldap.bind = { distinguishedName = "cn=admin,${dbSuffix}"; - password = "/etc/ldap/bind.password"; + passwordFile = "/etc/ldap/bind.password"; }; - # NOTE: password stored in clear in Nix's store, but this is a test. + # NOTE: passwords stored in clear in Nix's store, but this is a test. environment.etc."ldap/bind.password".source = pkgs.writeText "password" dbAdminPwd; + environment.etc."nslcd.rootpwmodpw".source = pkgs.writeText "rootpwmodpw" dbAdminPwd; }; in diff --git a/nixos/tests/mailcatcher.nix b/nixos/tests/mailcatcher.nix new file mode 100644 index 000000000000..d45b5d4edfc5 --- /dev/null +++ b/nixos/tests/mailcatcher.nix @@ -0,0 +1,26 @@ +import ./make-test.nix ({ lib, ... }: + +{ + name = "mailcatcher"; + meta.maintainers = [ lib.maintainers.aanderse ]; + + machine = + { pkgs, ... }: + { + services.mailcatcher.enable = true; + + networking.defaultMailServer.directDelivery = true; + networking.defaultMailServer.hostName = "localhost:1025"; + + environment.systemPackages = [ pkgs.mailutils ]; + }; + + testScript = '' + startAll; + + $machine->waitForUnit('mailcatcher.service'); + $machine->waitForOpenPort('1025'); + $machine->succeed('echo "this is the body of the email" | mail -s "subject" root@example.org'); + $machine->succeed('curl http://localhost:1080/messages/1.source') =~ /this is the body of the email/ or die; + ''; +}) diff --git a/nixos/tests/minio.nix b/nixos/tests/minio.nix index 40a599546650..f1218b537711 100644 --- a/nixos/tests/minio.nix +++ b/nixos/tests/minio.nix @@ -1,4 +1,24 @@ -import ./make-test.nix ({ pkgs, ...} : { +import ./make-test.nix ({ pkgs, ...} : +let + accessKey = "BKIKJAA5BMMU2RHO6IBB"; + secretKey = "V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12"; + minioPythonScript = pkgs.writeScript "minio-test.py" '' + #! ${pkgs.python3.withPackages(ps: [ ps.minio ])}/bin/python + import io + import os + from minio import Minio + minioClient = Minio('localhost:9000', + access_key='${accessKey}', + secret_key='${secretKey}', + secure=False) + sio = io.BytesIO() + sio.write(b'Test from Python') + sio.seek(0, os.SEEK_END) + sio_len = sio.tell() + sio.seek(0) + minioClient.put_object('test-bucket', 'test.txt', sio, sio_len, content_type='text/plain') + ''; + in { name = "minio"; meta = with pkgs.stdenv.lib.maintainers; { maintainers = [ bachp ]; @@ -8,8 +28,7 @@ import ./make-test.nix ({ pkgs, ...} : { machine = { pkgs, ... }: { services.minio = { enable = true; - accessKey = "BKIKJAA5BMMU2RHO6IBB"; - secretKey = "V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12"; + inherit accessKey secretKey; }; environment.systemPackages = [ pkgs.minio-client ]; @@ -25,9 +44,11 @@ import ./make-test.nix ({ pkgs, ...} : { $machine->waitForOpenPort(9000); # Create a test bucket on the server - $machine->succeed("mc config host add minio http://localhost:9000 BKIKJAA5BMMU2RHO6IBB V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12 S3v4"); + $machine->succeed("mc config host add minio http://localhost:9000 ${accessKey} ${secretKey} S3v4"); $machine->succeed("mc mb minio/test-bucket"); + $machine->succeed("${minioPythonScript}"); $machine->succeed("mc ls minio") =~ /test-bucket/ or die; + $machine->succeed("mc cat minio/test-bucket/test.txt") =~ /Test from Python/ or die; $machine->shutdown; ''; diff --git a/nixos/tests/mysql.nix b/nixos/tests/mysql.nix index 1a6117793664..fedc7f0ab1f0 100644 --- a/nixos/tests/mysql.nix +++ b/nixos/tests/mysql.nix @@ -5,7 +5,7 @@ import ./make-test.nix ({ pkgs, ...} : { }; nodes = { - master = + mysql = { pkgs, ... }: { @@ -13,12 +13,34 @@ import ./make-test.nix ({ pkgs, ...} : { services.mysql.initialDatabases = [ { name = "testdb"; schema = ./testdb.sql; } ]; services.mysql.package = pkgs.mysql; }; + + mariadb = + { pkgs, ... }: + + { + users.users.testuser = { }; + services.mysql.enable = true; + services.mysql.ensureDatabases = [ "testdb" ]; + services.mysql.ensureUsers = [{ + name = "testuser"; + ensurePermissions = { + "testdb.*" = "ALL PRIVILEGES"; + }; + }]; + services.mysql.package = pkgs.mariadb; + }; + }; testScript = '' startAll; - $master->waitForUnit("mysql"); - $master->succeed("echo 'use testdb; select * from tests' | mysql -u root -N | grep 4"); + $mysql->waitForUnit("mysql"); + $mysql->succeed("echo 'use testdb; select * from tests' | mysql -u root -N | grep 4"); + + $mariadb->waitForUnit("mysql"); + $mariadb->succeed("echo 'use testdb; create table tests (test_id INT, PRIMARY KEY (test_id));' | sudo -u testuser mysql -u testuser"); + $mariadb->succeed("echo 'use testdb; insert into tests values (42);' | sudo -u testuser mysql -u testuser"); + $mariadb->succeed("echo 'use testdb; select test_id from tests' | sudo -u testuser mysql -u testuser -N | grep 42"); ''; }) diff --git a/nixos/tests/predictable-interface-names.nix b/nixos/tests/predictable-interface-names.nix index 8306abb8c42f..85047f66f23c 100644 --- a/nixos/tests/predictable-interface-names.nix +++ b/nixos/tests/predictable-interface-names.nix @@ -20,8 +20,7 @@ in pkgs.lib.listToAttrs (pkgs.lib.crossLists (predictable: withNetworkd: { testScript = '' print $machine->succeed("ip link"); - $machine->succeed("ip link show ${if predictable then "ens3" else "eth0"}"); - $machine->fail("ip link show ${if predictable then "eth0" else "ens3"}"); + $machine->${if predictable then "fail" else "succeed"}("ip link show eth0 "); ''; }; }) [[true false] [true false]]) diff --git a/nixos/tests/systemd-confinement.nix b/nixos/tests/systemd-confinement.nix new file mode 100644 index 000000000000..b7b10fb36aac --- /dev/null +++ b/nixos/tests/systemd-confinement.nix @@ -0,0 +1,168 @@ +import ./make-test.nix { + name = "systemd-confinement"; + + machine = { pkgs, lib, ... }: let + testServer = pkgs.writeScript "testserver.sh" '' + #!${pkgs.stdenv.shell} + export PATH=${lib.escapeShellArg "${pkgs.coreutils}/bin"} + ${lib.escapeShellArg pkgs.stdenv.shell} 2>&1 + echo "exit-status:$?" + ''; + + testClient = pkgs.writeScriptBin "chroot-exec" '' + #!${pkgs.stdenv.shell} -e + output="$(echo "$@" | nc -NU "/run/test$(< /teststep).sock")" + ret="$(echo "$output" | sed -nre '$s/^exit-status:([0-9]+)$/\1/p')" + echo "$output" | head -n -1 + exit "''${ret:-1}" + ''; + + mkTestStep = num: { description, config ? {}, testScript }: { + systemd.sockets."test${toString num}" = { + description = "Socket for Test Service ${toString num}"; + wantedBy = [ "sockets.target" ]; + socketConfig.ListenStream = "/run/test${toString num}.sock"; + socketConfig.Accept = true; + }; + + systemd.services."test${toString num}@" = { + description = "Confined Test Service ${toString num}"; + confinement = (config.confinement or {}) // { enable = true; }; + serviceConfig = (config.serviceConfig or {}) // { + ExecStart = testServer; + StandardInput = "socket"; + }; + } // removeAttrs config [ "confinement" "serviceConfig" ]; + + __testSteps = lib.mkOrder num '' + subtest '${lib.escape ["\\" "'"] description}', sub { + $machine->succeed('echo ${toString num} > /teststep'); + ${testScript} + }; + ''; + }; + + in { + imports = lib.imap1 mkTestStep [ + { description = "chroot-only confinement"; + config.confinement.mode = "chroot-only"; + testScript = '' + $machine->succeed( + 'test "$(chroot-exec ls -1 / | paste -sd,)" = bin,nix', + 'test "$(chroot-exec id -u)" = 0', + 'chroot-exec chown 65534 /bin', + ); + ''; + } + { description = "full confinement with APIVFS"; + testScript = '' + $machine->fail( + 'chroot-exec ls -l /etc', + 'chroot-exec ls -l /run', + 'chroot-exec chown 65534 /bin', + ); + $machine->succeed( + 'test "$(chroot-exec id -u)" = 0', + 'chroot-exec chown 0 /bin', + ); + ''; + } + { description = "check existence of bind-mounted /etc"; + config.serviceConfig.BindReadOnlyPaths = [ "/etc" ]; + testScript = '' + $machine->succeed('test -n "$(chroot-exec cat /etc/passwd)"'); + ''; + } + { description = "check if User/Group really runs as non-root"; + config.serviceConfig.User = "chroot-testuser"; + config.serviceConfig.Group = "chroot-testgroup"; + testScript = '' + $machine->succeed('chroot-exec ls -l /dev'); + $machine->succeed('test "$(chroot-exec id -u)" != 0'); + $machine->fail('chroot-exec touch /bin/test'); + ''; + } + (let + symlink = pkgs.runCommand "symlink" { + target = pkgs.writeText "symlink-target" "got me\n"; + } "ln -s \"$target\" \"$out\""; + in { + description = "check if symlinks are properly bind-mounted"; + config.confinement.packages = lib.singleton symlink; + testScript = '' + $machine->fail('chroot-exec test -e /etc'); + $machine->succeed('chroot-exec cat ${symlink} >&2'); + $machine->succeed('test "$(chroot-exec cat ${symlink})" = "got me"'); + ''; + }) + { description = "check if StateDirectory works"; + config.serviceConfig.User = "chroot-testuser"; + config.serviceConfig.Group = "chroot-testgroup"; + config.serviceConfig.StateDirectory = "testme"; + testScript = '' + $machine->succeed('chroot-exec touch /tmp/canary'); + $machine->succeed('chroot-exec "echo works > /var/lib/testme/foo"'); + $machine->succeed('test "$(< /var/lib/testme/foo)" = works'); + $machine->succeed('test ! -e /tmp/canary'); + ''; + } + { description = "check if /bin/sh works"; + testScript = '' + $machine->succeed( + 'chroot-exec test -e /bin/sh', + 'test "$(chroot-exec \'/bin/sh -c "echo bar"\')" = bar', + ); + ''; + } + { description = "check if suppressing /bin/sh works"; + config.confinement.binSh = null; + testScript = '' + $machine->succeed( + 'chroot-exec test ! -e /bin/sh', + 'test "$(chroot-exec \'/bin/sh -c "echo foo"\')" != foo', + ); + ''; + } + { description = "check if we can set /bin/sh to something different"; + config.confinement.binSh = "${pkgs.hello}/bin/hello"; + testScript = '' + $machine->succeed( + 'chroot-exec test -e /bin/sh', + 'test "$(chroot-exec /bin/sh -g foo)" = foo', + ); + ''; + } + { description = "check if only Exec* dependencies are included"; + config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n"; + testScript = '' + $machine->succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" != eek'); + ''; + } + { description = "check if all unit dependencies are included"; + config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n"; + config.confinement.fullUnit = true; + testScript = '' + $machine->succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" = eek'); + ''; + } + ]; + + options.__testSteps = lib.mkOption { + type = lib.types.lines; + description = "All of the test steps combined as a single script."; + }; + + config.environment.systemPackages = lib.singleton testClient; + + config.users.groups.chroot-testgroup = {}; + config.users.users.chroot-testuser = { + description = "Chroot Test User"; + group = "chroot-testgroup"; + }; + }; + + testScript = { nodes, ... }: '' + $machine->waitForUnit('multi-user.target'); + ${nodes.machine.config.__testSteps} + ''; +} |