From 5506463c2f63a9a111191de97aab0ef9c3d87124 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Wed, 1 Apr 2020 14:01:41 +0200 Subject: [PATCH 1/3] google-compute-engine-oslogin: 1.5.3 -> 20200325.00 Google moved their oslogin guest tools to another repository. Point src to there, and bump to the latest version There's now a Makefile, so we can avoid having our own custom installPhase, and we also get manpages. I successfully ran the oslogin tests, so assuming the google cloud metadata server still behaves like in our test, logins should work. I saw a nscd segfault, not sure if it's caused by this or was already the case before. It'd be great if someone could test this on an actual VM. --- .../google-compute-engine-oslogin/default.nix | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/pkgs/tools/virtualization/google-compute-engine-oslogin/default.nix b/pkgs/tools/virtualization/google-compute-engine-oslogin/default.nix index 31ea1cb2a946..2c2444de4e52 100644 --- a/pkgs/tools/virtualization/google-compute-engine-oslogin/default.nix +++ b/pkgs/tools/virtualization/google-compute-engine-oslogin/default.nix @@ -1,4 +1,5 @@ { stdenv +, lib , fetchFromGitHub , curl , json_c @@ -8,22 +9,20 @@ stdenv.mkDerivation rec { pname = "google-compute-engine-oslogin"; - version = "1.5.3"; - # from packages/google-compute-engine-oslogin/packaging/debian/changelog + version = "20200325.00"; src = fetchFromGitHub { owner = "GoogleCloudPlatform"; - repo = "compute-image-packages"; - rev = "20190522"; - sha256 = "16jbbrnz49g843h813r408dbvfa2hicf8canxwbfxr2kzhv7ycmm"; + repo = "guest-oslogin"; + rev = version; + sha256 = "03hk95pgzcgy6ginp8zdy0fbk88m6n65qq22jq490z1xwbjffm8r"; }; - sourceRoot = "source/packages/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 + substituteInPlace src/pam/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 compat.h --replace /bin/bash ${bashInteractive}/bin/bash + substituteInPlace src/include/compat.h --replace /bin/bash ${bashInteractive}/bin/bash ''; buildInputs = [ curl.dev pam ]; @@ -31,15 +30,15 @@ stdenv.mkDerivation rec { 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 - ''; + makeFlags = [ + "VERSION=${version}" + "DESTDIR=${placeholder "out"}" + "PREFIX=/" + "BINDIR=/bin" + "LIBDIR=/lib" + "PAMDIR=/lib" + "MANDIR=/share/man" + ]; enableParallelBuilding = true; From f38e45c2e0ea15c1882308299fbe24f6c46b8243 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Wed, 1 Apr 2020 15:53:56 +0200 Subject: [PATCH 2/3] nixos/google-oslogin: improve mock server some slightly better error handling for nonexistent users, less parsing of URLs and query strings by hand. --- nixos/tests/google-oslogin/server.py | 77 ++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/nixos/tests/google-oslogin/server.py b/nixos/tests/google-oslogin/server.py index bfc527cb97d3..eb0c77982d01 100644 --- a/nixos/tests/google-oslogin/server.py +++ b/nixos/tests/google-oslogin/server.py @@ -7,24 +7,27 @@ import hashlib import base64 from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import urlparse, parse_qs from typing import Dict SNAKEOIL_PUBLIC_KEY = os.environ['SNAKEOIL_PUBLIC_KEY'] -def w(msg): +def w(msg: bytes): sys.stderr.write(f"{msg}\n") sys.stderr.flush() -def gen_fingerprint(pubkey): +def gen_fingerprint(pubkey: str): decoded_key = base64.b64decode(pubkey.encode("ascii").split()[1]) return hashlib.sha256(decoded_key).hexdigest() -def gen_email(username): + +def gen_email(username: str): """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 @@ -56,7 +59,8 @@ def gen_mockuser(username: str, uid: str, gid: str, home_directory: str, snakeoi class ReqHandler(BaseHTTPRequestHandler): - def _send_json_ok(self, data): + + def _send_json_ok(self, data: dict): self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() @@ -64,29 +68,62 @@ class ReqHandler(BaseHTTPRequestHandler): w(out) self.wfile.write(out) + def _send_json_success(self, success=True): + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + out = json.dumps({"success": success}).encode() + w(out) + self.wfile.write(out) + + def _send_404(self): + self.send_response(404) + self.end_headers() + 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)) + pu = urlparse(p) + params = parse_qs(pu.query) - # mockuser is allowed to login - elif p == f"/computeMetadata/v1/oslogin/authorize?email={gen_email('mockuser')}&policy=login": - self._send_json_ok({'success': True}) + # users endpoint + if pu.path == "/computeMetadata/v1/oslogin/users": + # mockuser and mockadmin are allowed to login, both use the same snakeoil public key + if params.get('username') == ['mockuser'] or params.get('uid') == ["1009719690"]: + username = "mockuser" + uid = "1009719690" + elif params.get('username') == ['mockadmin'] or params.get('uid') == ["1009719691"]: + username = "mockadmin" + uid = "1009719691" + else: + self._send_404() + return - # 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}) + self._send_json_ok(gen_mockuser(username=username, uid=uid, gid=uid, home_directory=f"/home/{username}", snakeoil_pubkey=SNAKEOIL_PUBLIC_KEY)) + return + + # authorize endpoint + elif pu.path == "/computeMetadata/v1/oslogin/authorize": + # is user allowed to login? + if params.get("policy") == ["login"]: + # mockuser and mockadmin are allowed to login + if params.get('email') == [gen_email("mockuser")] or params.get('email') == [gen_email("mockadmin")]: + self._send_json_success() + return + self._send_json_success(False) + return + # is user allowed to become root? + elif params.get("policy") == ["adminLogin"]: + # only mockadmin is allowed to become admin + self._send_json_success((params['email'] == [gen_email("mockadmin")])) + return + # send 404 for other policies + else: + self._send_404() + return else: sys.stderr.write(f"Unhandled path: {p}\n") sys.stderr.flush() - self.send_response(501) + self.send_response(404) self.end_headers() self.wfile.write(b'') From 21da5c4f6f8a63475545751aee53552ee9bc72eb Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Wed, 1 Apr 2020 16:21:38 +0200 Subject: [PATCH 3/3] nixos/oslogin: put mockuser and mockadmin in constants, rename This allows us to change them easily without search/replacing. Afterwards, we rename them to look a bit more like they are on GCP. --- nixos/tests/google-oslogin/default.nix | 18 +++++++++++------- nixos/tests/google-oslogin/server.py | 14 ++++++++------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/nixos/tests/google-oslogin/default.nix b/nixos/tests/google-oslogin/default.nix index 1977e92e9877..97783c81f397 100644 --- a/nixos/tests/google-oslogin/default.nix +++ b/nixos/tests/google-oslogin/default.nix @@ -22,6 +22,8 @@ in { client = { ... }: {}; }; testScript = '' + MOCKUSER = "mockuser_nixos_org" + MOCKADMIN = "mockadmin_nixos_org" start_all() server.wait_for_unit("mock-google-metadata.service") @@ -29,10 +31,10 @@ in { # 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}"' + f'${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}"' + f'${pkgs.google-compute-engine-oslogin}/bin/google_authorized_keys {MOCKADMIN} | grep -q "${snakeOilPublicKey}"' ) # install snakeoil ssh key on the client, and provision .ssh/config file @@ -50,20 +52,22 @@ in { client.fail("ssh ghost@server 'true'") # we should be able to connect as mockuser - client.succeed("ssh mockuser@server 'true'") + client.succeed(f"ssh {MOCKUSER}@server 'true'") # but we shouldn't be able to sudo client.fail( - "ssh mockuser@server '/run/wrappers/bin/sudo /run/current-system/sw/bin/id' | grep -q 'root'" + f"ssh {MOCKUSER}@server '/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 mockadmin@server 'true'") + client.succeed(f"ssh {MOCKADMIN}@server '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'") + server.succeed( + f"find /run/google-sudoers.d | grep -q '/run/google-sudoers.d/{MOCKADMIN}'" + ) # and we should be able to sudo client.succeed( - "ssh mockadmin@server '/run/wrappers/bin/sudo /run/current-system/sw/bin/id' | grep -q 'root'" + f"ssh {MOCKADMIN}@server '/run/wrappers/bin/sudo /run/current-system/sw/bin/id' | grep -q 'root'" ) ''; }) diff --git a/nixos/tests/google-oslogin/server.py b/nixos/tests/google-oslogin/server.py index eb0c77982d01..5ea9bbd2c96b 100644 --- a/nixos/tests/google-oslogin/server.py +++ b/nixos/tests/google-oslogin/server.py @@ -11,6 +11,8 @@ from urllib.parse import urlparse, parse_qs from typing import Dict SNAKEOIL_PUBLIC_KEY = os.environ['SNAKEOIL_PUBLIC_KEY'] +MOCKUSER="mockuser_nixos_org" +MOCKADMIN="mockadmin_nixos_org" def w(msg: bytes): @@ -88,11 +90,11 @@ class ReqHandler(BaseHTTPRequestHandler): # users endpoint if pu.path == "/computeMetadata/v1/oslogin/users": # mockuser and mockadmin are allowed to login, both use the same snakeoil public key - if params.get('username') == ['mockuser'] or params.get('uid') == ["1009719690"]: - username = "mockuser" + if params.get('username') == [MOCKUSER] or params.get('uid') == ["1009719690"]: + username = MOCKUSER uid = "1009719690" - elif params.get('username') == ['mockadmin'] or params.get('uid') == ["1009719691"]: - username = "mockadmin" + elif params.get('username') == [MOCKADMIN] or params.get('uid') == ["1009719691"]: + username = MOCKADMIN uid = "1009719691" else: self._send_404() @@ -106,7 +108,7 @@ class ReqHandler(BaseHTTPRequestHandler): # is user allowed to login? if params.get("policy") == ["login"]: # mockuser and mockadmin are allowed to login - if params.get('email') == [gen_email("mockuser")] or params.get('email') == [gen_email("mockadmin")]: + if params.get('email') == [gen_email(MOCKUSER)] or params.get('email') == [gen_email(MOCKADMIN)]: self._send_json_success() return self._send_json_success(False) @@ -114,7 +116,7 @@ class ReqHandler(BaseHTTPRequestHandler): # is user allowed to become root? elif params.get("policy") == ["adminLogin"]: # only mockadmin is allowed to become admin - self._send_json_success((params['email'] == [gen_email("mockadmin")])) + self._send_json_success((params['email'] == [gen_email(MOCKADMIN)])) return # send 404 for other policies else: