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 { };