diff --git a/nixos/doc/manual/release-notes/rl-1903.xml b/nixos/doc/manual/release-notes/rl-1903.xml
index 7bc88769337..d99c8881727 100644
--- a/nixos/doc/manual/release-notes/rl-1903.xml
+++ b/nixos/doc/manual/release-notes/rl-1903.xml
@@ -43,6 +43,15 @@
./programs/nm-applet.nix
+
+
+ There is a new security.googleOsLogin module for using
+ OS Login
+ to manage SSH access to Google Compute Engine instances, which supersedes
+ the imperative and broken google-accounts-daemon used
+ in nixos/modules/virtualisation/google-compute-config.nix.
+
+
diff --git a/nixos/modules/config/nsswitch.nix b/nixos/modules/config/nsswitch.nix
index a74d551f50d..b601e908e49 100644
--- a/nixos/modules/config/nsswitch.nix
+++ b/nixos/modules/config/nsswitch.nix
@@ -1,6 +1,6 @@
# Configuration for the Name Service Switch (/etc/nsswitch.conf).
-{ config, lib, ... }:
+{ config, lib, pkgs, ... }:
with lib;
@@ -15,6 +15,7 @@ let
ldap = canLoadExternalModules && (config.users.ldap.enable && config.users.ldap.nsswitch);
sssd = canLoadExternalModules && config.services.sssd.enable;
resolved = canLoadExternalModules && config.services.resolved.enable;
+ googleOsLogin = canLoadExternalModules && config.security.googleOsLogin.enable;
hostArray = [ "files" ]
++ optional mymachines "mymachines"
@@ -29,6 +30,7 @@ let
++ optional sssd "sss"
++ optional ldap "ldap"
++ optional mymachines "mymachines"
+ ++ optional googleOsLogin "cache_oslogin oslogin"
++ [ "systemd" ];
shadowArray = [ "files" ]
@@ -97,7 +99,7 @@ in {
# configured IP addresses, or ::1 and 127.0.0.2 as
# fallbacks. Systemd also provides nss-mymachines to return IP
# addresses of local containers.
- system.nssModules = optionals canLoadExternalModules [ config.systemd.package.out ];
-
+ system.nssModules = (optionals canLoadExternalModules [ config.systemd.package.out ])
+ ++ optional googleOsLogin pkgs.google-compute-engine-oslogin.out;
};
}
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 11d0205b180..4a392b6f5c9 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -154,6 +154,7 @@
./security/chromium-suid-sandbox.nix
./security/dhparams.nix
./security/duosec.nix
+ ./security/google_oslogin.nix
./security/hidepid.nix
./security/lock-kernel-modules.nix
./security/misc.nix
diff --git a/nixos/modules/security/google_oslogin.nix b/nixos/modules/security/google_oslogin.nix
new file mode 100644
index 00000000000..246419b681a
--- /dev/null
+++ b/nixos/modules/security/google_oslogin.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.security.googleOsLogin;
+ package = pkgs.google-compute-engine-oslogin;
+
+in
+
+{
+
+ options = {
+
+ security.googleOsLogin.enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to enable Google OS Login
+
+ The OS Login package enables the following components:
+ AuthorizedKeysCommand to query valid SSH keys from the user's OS Login
+ profile during ssh authentication phase.
+ NSS Module to provide user and group information
+ PAM Module for the sshd service, providing authorization and
+ authentication support, allowing the system to use data stored in
+ Google Cloud IAM permissions to control both, the ability to log into
+ an instance, and to perform operations as root (sudo).
+ '';
+ };
+
+ };
+
+ config = mkIf cfg.enable {
+ security.pam.services.sshd = {
+ makeHomeDir = true;
+ googleOsLoginAccountVerification = true;
+ # disabled for now: googleOsLoginAuthentication = true;
+ };
+
+ security.sudo.extraConfig = ''
+ #includedir /run/google-sudoers.d
+ '';
+ systemd.tmpfiles.rules = [
+ "d /run/google-sudoers.d 750 root root -"
+ "d /var/google-users.d 750 root root -"
+ ];
+
+ # enable the nss module, so user lookups etc. work
+ system.nssModules = [ package ];
+
+ # Ugly: sshd refuses to start if a store path is given because /nix/store is group-writable.
+ # So indirect by a symlink.
+ environment.etc."ssh/authorized_keys_command_google_oslogin" = {
+ mode = "0755";
+ text = ''
+ #!/bin/sh
+ exec ${package}/bin/google_authorized_keys "$@"
+ '';
+ };
+ services.openssh.extraConfig = ''
+ AuthorizedKeysCommand /etc/ssh/authorized_keys_command_google_oslogin %u
+ AuthorizedKeysCommandUser nobody
+ '';
+ };
+
+}
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
index 812a71c68a3..b1a0eff98c2 100644
--- a/nixos/modules/security/pam.nix
+++ b/nixos/modules/security/pam.nix
@@ -77,6 +77,30 @@ let
'';
};
+ googleOsLoginAccountVerification = mkOption {
+ default = false;
+ type = types.bool;
+ description = ''
+ If set, will use the Google OS Login PAM modules
+ (pam_oslogin_login,
+ pam_oslogin_admin) to verify possible OS Login
+ users and set sudoers configuration accordingly.
+ This only makes sense to enable for the sshd PAM
+ service.
+ '';
+ };
+
+ googleOsLoginAuthentication = mkOption {
+ default = false;
+ type = types.bool;
+ description = ''
+ If set, will use the pam_oslogin_login's user
+ authentication methods to authenticate users using 2FA.
+ This only makes sense to enable for the sshd PAM
+ service.
+ '';
+ };
+
fprintAuth = mkOption {
default = config.services.fprintd.enable;
type = types.bool;
@@ -278,8 +302,14 @@ let
"account [default=bad success=ok user_unknown=ignore] ${pkgs.sssd}/lib/security/pam_sss.so"}
${optionalString config.krb5.enable
"account sufficient ${pam_krb5}/lib/security/pam_krb5.so"}
+ ${optionalString cfg.googleOsLoginAccountVerification ''
+ account [success=ok ignore=ignore default=die] ${pkgs.google-compute-engine-oslogin}/lib/pam_oslogin_login.so
+ account [success=ok default=ignore] ${pkgs.google-compute-engine-oslogin}/lib/pam_oslogin_admin.so
+ ''}
# Authentication management.
+ ${optionalString cfg.googleOsLoginAuthentication
+ "auth [success=done perm_denied=bad default=ignore] ${pkgs.google-compute-engine-oslogin}/lib/pam_oslogin_login.so"}
${optionalString cfg.rootOK
"auth sufficient pam_rootok.so"}
${optionalString cfg.requireWheel
diff --git a/nixos/modules/virtualisation/google-compute-config.nix b/nixos/modules/virtualisation/google-compute-config.nix
index 1f8485b274f..8c7331fe4d2 100644
--- a/nixos/modules/virtualisation/google-compute-config.nix
+++ b/nixos/modules/virtualisation/google-compute-config.nix
@@ -65,33 +65,7 @@ in
# GC has 1460 MTU
networking.interfaces.eth0.mtu = 1460;
- # allow the google-accounts-daemon to manage users
- users.mutableUsers = true;
- # and allow users to sudo without password
- security.sudo.enable = true;
- security.sudo.extraConfig = ''
- %google-sudoers ALL=(ALL:ALL) NOPASSWD:ALL
- '';
-
- # NOTE: google-accounts tries to write to /etc/sudoers.d but the folder doesn't exist
- # FIXME: not such file or directory on dynamic SSH provisioning
- systemd.services.google-accounts-daemon = {
- description = "Google Compute Engine Accounts Daemon";
- # This daemon creates dynamic users
- enable = config.users.mutableUsers;
- after = [
- "network.target"
- "google-instance-setup.service"
- "google-network-setup.service"
- ];
- requires = ["network.target"];
- wantedBy = ["multi-user.target"];
- path = with pkgs; [ shadow ];
- serviceConfig = {
- Type = "simple";
- ExecStart = "${gce}/bin/google_accounts_daemon --debug";
- };
- };
+ security.googleOsLogin.enable = true;
systemd.services.google-clock-skew-daemon = {
description = "Google Compute Engine Clock Skew Daemon";
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 1e8e2213fac..860262eeb6c 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -81,6 +81,7 @@ in
gitlab = handleTest ./gitlab.nix {};
gitolite = handleTest ./gitolite.nix {};
gjs = handleTest ./gjs.nix {};
+ google-oslogin = handleTest ./google-oslogin {};
gnome3 = handleTestOn ["x86_64-linux"] ./gnome3.nix {}; # libsmbios is unsupported on aarch64
gnome3-gdm = handleTestOn ["x86_64-linux"] ./gnome3-gdm.nix {}; # libsmbios is unsupported on aarch64
gocd-agent = handleTest ./gocd-agent.nix {};
diff --git a/nixos/tests/google-oslogin/default.nix b/nixos/tests/google-oslogin/default.nix
new file mode 100644
index 00000000000..3b84bba3f98
--- /dev/null
+++ b/nixos/tests/google-oslogin/default.nix
@@ -0,0 +1,52 @@
+import ../make-test.nix ({ pkgs, ... } :
+let
+ inherit (import ./../ssh-keys.nix pkgs)
+ snakeOilPrivateKey snakeOilPublicKey;
+in {
+ name = "google-oslogin";
+ meta = with pkgs.stdenv.lib.maintainers; {
+ maintainers = [ adisbladis flokli ];
+ };
+
+ nodes = {
+ # the server provides both the the mocked google metadata server and the ssh server
+ server = (import ./server.nix pkgs);
+
+ client = { ... }: {};
+ };
+ testScript = ''
+ startAll;
+
+ $server->waitForUnit("mock-google-metadata.service");
+ $server->waitForOpenPort(80);
+
+ # mockserver should return a non-expired ssh key for both mockuser and mockadmin
+ $server->succeed('${pkgs.google-compute-engine-oslogin}/bin/google_authorized_keys mockuser | grep -q "${snakeOilPublicKey}"');
+ $server->succeed('${pkgs.google-compute-engine-oslogin}/bin/google_authorized_keys mockadmin | grep -q "${snakeOilPublicKey}"');
+
+ # install snakeoil ssh key on the client
+ $client->succeed("mkdir -p ~/.ssh");
+ $client->succeed("cat ${snakeOilPrivateKey} > ~/.ssh/id_snakeoil");
+ $client->succeed("chmod 600 ~/.ssh/id_snakeoil");
+
+ $client->waitForUnit("network.target");
+ $server->waitForUnit("sshd.service");
+
+ # we should not be able to connect as non-existing user
+ $client->fail("ssh -o User=ghost -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server -i ~/.ssh/id_snakeoil 'true'");
+
+ # we should be able to connect as mockuser
+ $client->succeed("ssh -o User=mockuser -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server -i ~/.ssh/id_snakeoil 'true'");
+ # but we shouldn't be able to sudo
+ $client->fail("ssh -o User=mockuser -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server -i ~/.ssh/id_snakeoil '/run/wrappers/bin/sudo /run/current-system/sw/bin/id' | grep -q 'root'");
+
+ # we should also be able to log in as mockadmin
+ $client->succeed("ssh -o User=mockadmin -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server -i ~/.ssh/id_snakeoil 'true'");
+ # pam_oslogin_admin.so should now have generated a sudoers file
+ $server->succeed("find /run/google-sudoers.d | grep -q '/run/google-sudoers.d/mockadmin'");
+
+ # and we should be able to sudo
+ $client->succeed("ssh -o User=mockadmin -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server -i ~/.ssh/id_snakeoil '/run/wrappers/bin/sudo /run/current-system/sw/bin/id' | grep -q 'root'");
+ '';
+ })
+
diff --git a/nixos/tests/google-oslogin/server.nix b/nixos/tests/google-oslogin/server.nix
new file mode 100644
index 00000000000..fdb7141da31
--- /dev/null
+++ b/nixos/tests/google-oslogin/server.nix
@@ -0,0 +1,29 @@
+{ pkgs, ... }:
+let
+ inherit (import ./../ssh-keys.nix pkgs)
+ snakeOilPrivateKey snakeOilPublicKey;
+in {
+ networking.firewall.allowedTCPPorts = [ 80 ];
+
+ systemd.services.mock-google-metadata = {
+ description = "Mock Google metadata service";
+ serviceConfig.Type = "simple";
+ serviceConfig.ExecStart = "${pkgs.python3}/bin/python ${./server.py}";
+ environment = {
+ SNAKEOIL_PUBLIC_KEY = snakeOilPublicKey;
+ };
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+ };
+
+ services.openssh.enable = true;
+ services.openssh.challengeResponseAuthentication = false;
+ services.openssh.passwordAuthentication = false;
+
+ security.googleOsLogin.enable = true;
+
+ # Mock google service
+ networking.extraHosts = ''
+ 127.0.0.1 metadata.google.internal
+ '';
+}
diff --git a/nixos/tests/google-oslogin/server.py b/nixos/tests/google-oslogin/server.py
new file mode 100644
index 00000000000..bfc527cb97d
--- /dev/null
+++ b/nixos/tests/google-oslogin/server.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+import json
+import sys
+import time
+import os
+import hashlib
+import base64
+
+from http.server import BaseHTTPRequestHandler, HTTPServer
+from typing import Dict
+
+SNAKEOIL_PUBLIC_KEY = os.environ['SNAKEOIL_PUBLIC_KEY']
+
+
+def w(msg):
+ sys.stderr.write(f"{msg}\n")
+ sys.stderr.flush()
+
+
+def gen_fingerprint(pubkey):
+ decoded_key = base64.b64decode(pubkey.encode("ascii").split()[1])
+ return hashlib.sha256(decoded_key).hexdigest()
+
+def gen_email(username):
+ """username seems to be a 21 characters long number string, so mimic that in a reproducible way"""
+ return str(int(hashlib.sha256(username.encode()).hexdigest(), 16))[0:21]
+
+def gen_mockuser(username: str, uid: str, gid: str, home_directory: str, snakeoil_pubkey: str) -> Dict:
+ snakeoil_pubkey_fingerprint = gen_fingerprint(snakeoil_pubkey)
+ # seems to be a 21 characters long numberstring, so mimic that in a reproducible way
+ email = gen_email(username)
+ return {
+ "loginProfiles": [
+ {
+ "name": email,
+ "posixAccounts": [
+ {
+ "primary": True,
+ "username": username,
+ "uid": uid,
+ "gid": gid,
+ "homeDirectory": home_directory,
+ "operatingSystemType": "LINUX"
+ }
+ ],
+ "sshPublicKeys": {
+ snakeoil_pubkey_fingerprint: {
+ "key": snakeoil_pubkey,
+ "expirationTimeUsec": str((time.time() + 600) * 1000000), # 10 minutes in the future
+ "fingerprint": snakeoil_pubkey_fingerprint
+ }
+ }
+ }
+ ]
+ }
+
+
+class ReqHandler(BaseHTTPRequestHandler):
+ def _send_json_ok(self, data):
+ self.send_response(200)
+ self.send_header('Content-type', 'application/json')
+ self.end_headers()
+ out = json.dumps(data).encode()
+ w(out)
+ self.wfile.write(out)
+
+ def do_GET(self):
+ p = str(self.path)
+ # mockuser and mockadmin are allowed to login, both use the same snakeoil public key
+ if p == '/computeMetadata/v1/oslogin/users?username=mockuser' \
+ or p == '/computeMetadata/v1/oslogin/users?uid=1009719690':
+ self._send_json_ok(gen_mockuser(username='mockuser', uid='1009719690', gid='1009719690',
+ home_directory='/home/mockuser', snakeoil_pubkey=SNAKEOIL_PUBLIC_KEY))
+ elif p == '/computeMetadata/v1/oslogin/users?username=mockadmin' \
+ or p == '/computeMetadata/v1/oslogin/users?uid=1009719691':
+ self._send_json_ok(gen_mockuser(username='mockadmin', uid='1009719691', gid='1009719691',
+ home_directory='/home/mockadmin', snakeoil_pubkey=SNAKEOIL_PUBLIC_KEY))
+
+ # mockuser is allowed to login
+ elif p == f"/computeMetadata/v1/oslogin/authorize?email={gen_email('mockuser')}&policy=login":
+ self._send_json_ok({'success': True})
+
+ # mockadmin may also become root
+ elif p == f"/computeMetadata/v1/oslogin/authorize?email={gen_email('mockadmin')}&policy=login" or p == f"/computeMetadata/v1/oslogin/authorize?email={gen_email('mockadmin')}&policy=adminLogin":
+ self._send_json_ok({'success': True})
+ else:
+ sys.stderr.write(f"Unhandled path: {p}\n")
+ sys.stderr.flush()
+ self.send_response(501)
+ self.end_headers()
+ self.wfile.write(b'')
+
+
+if __name__ == '__main__':
+ s = HTTPServer(('0.0.0.0', 80), ReqHandler)
+ s.serve_forever()
diff --git a/pkgs/tools/virtualization/google-compute-engine-oslogin/default.nix b/pkgs/tools/virtualization/google-compute-engine-oslogin/default.nix
new file mode 100644
index 00000000000..5096c7f9468
--- /dev/null
+++ b/pkgs/tools/virtualization/google-compute-engine-oslogin/default.nix
@@ -0,0 +1,48 @@
+{ stdenv
+, fetchFromGitHub
+, curl
+, json_c
+, pam
+}:
+
+stdenv.mkDerivation rec {
+ name = "google-compute-engine-oslogin-${version}";
+ version = "1.4.3";
+
+ src = fetchFromGitHub {
+ repo = "compute-image-packages";
+ owner = "GoogleCloudPlatform";
+ rev = "2ccfe80f162a01b5b7c3316ca37981fc8b3fc32a";
+ sha256 = "036g7609ni164rmm68pzi47vrywfz2rcv0ad67gqf331pvlr92x1";
+ };
+ sourceRoot = "source/google_compute_engine_oslogin";
+
+ postPatch = ''
+ # change sudoers dir from /var/google-sudoers.d to /run/google-sudoers.d (managed through systemd-tmpfiles)
+ substituteInPlace pam_module/pam_oslogin_admin.cc --replace /var/google-sudoers.d /run/google-sudoers.d
+ # fix "User foo not allowed because shell /bin/bash does not exist"
+ substituteInPlace utils/oslogin_utils.cc --replace /bin/bash /bin/sh
+ '';
+
+ buildInputs = [ curl.dev pam ];
+
+ NIX_CFLAGS_COMPILE="-I${json_c.dev}/include/json-c";
+ NIX_CFLAGS_LINK="-L${json_c}/lib";
+
+ installPhase = ''
+ mkdir -p $out/{bin,lib}
+
+ install -Dm755 libnss_cache_google-compute-engine-oslogin-${version}.so $out/lib/libnss_cache_oslogin.so.2
+ install -Dm755 libnss_google-compute-engine-oslogin-${version}.so $out/lib/libnss_oslogin.so.2
+
+ install -Dm755 pam_oslogin_admin.so pam_oslogin_login.so $out/lib
+ install -Dm755 google_{oslogin_nss_cache,authorized_keys} $out/bin
+ '';
+
+ meta = with stdenv.lib; {
+ homepage = https://github.com/GoogleCloudPlatform/compute-image-packages;
+ description = "OS Login Guest Environment for Google Compute Engine";
+ license = licenses.asl20;
+ maintainers = with maintainers; [ adisbladis flokli ];
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index f073dc8086b..c7a4b579367 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -3039,6 +3039,8 @@ in
google-compute-engine = python2.pkgs.google-compute-engine;
+ google-compute-engine-oslogin = callPackage ../tools/virtualization/google-compute-engine-oslogin { };
+
gource = callPackage ../applications/version-management/gource { };
govc = callPackage ../tools/virtualization/govc { };