Skip to content

Commit

Permalink
Merge pull request #51566 from adisbladis/google-oslogin
Browse files Browse the repository at this point in the history
GCE OSLogin module: init
  • Loading branch information
zimbatm committed Dec 24, 2018
2 parents 8d217ed + 3539f38 commit d06f798
Show file tree
Hide file tree
Showing 12 changed files with 342 additions and 30 deletions.
9 changes: 9 additions & 0 deletions nixos/doc/manual/release-notes/rl-1903.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@
<literal>./programs/nm-applet.nix</literal>
</para>
</listitem>
<listitem>
<para>
There is a new <varname>security.googleOsLogin</varname> module for using
<link xlink:href="https://cloud.google.com/compute/docs/instances/managing-instance-access">OS Login</link>
to manage SSH access to Google Compute Engine instances, which supersedes
the imperative and broken <literal>google-accounts-daemon</literal> used
in <literal>nixos/modules/virtualisation/google-compute-config.nix</literal>.
</para>
</listitem>
</itemizedlist>
</section>

Expand Down
8 changes: 5 additions & 3 deletions nixos/modules/config/nsswitch.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Configuration for the Name Service Switch (/etc/nsswitch.conf).

{ config, lib, ... }:
{ config, lib, pkgs, ... }:

with lib;

Expand All @@ -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"
Expand All @@ -29,6 +30,7 @@ let
++ optional sssd "sss"
++ optional ldap "ldap"
++ optional mymachines "mymachines"
++ optional googleOsLogin "cache_oslogin oslogin"
++ [ "systemd" ];

shadowArray = [ "files" ]
Expand Down Expand Up @@ -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;
};
}
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 68 additions & 0 deletions nixos/modules/security/google_oslogin.nix
Original file line number Diff line number Diff line change
@@ -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
'';
};

}
30 changes: 30 additions & 0 deletions nixos/modules/security/pam.nix
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,30 @@ let
'';
};

googleOsLoginAccountVerification = mkOption {
default = false;
type = types.bool;
description = ''
If set, will use the Google OS Login PAM modules
(<literal>pam_oslogin_login</literal>,
<literal>pam_oslogin_admin</literal>) to verify possible OS Login
users and set sudoers configuration accordingly.
This only makes sense to enable for the <literal>sshd</literal> PAM
service.
'';
};

googleOsLoginAuthentication = mkOption {
default = false;
type = types.bool;
description = ''
If set, will use the <literal>pam_oslogin_login</literal>'s user
authentication methods to authenticate users using 2FA.
This only makes sense to enable for the <literal>sshd</literal> PAM
service.
'';
};

fprintAuth = mkOption {
default = config.services.fprintd.enable;
type = types.bool;
Expand Down Expand Up @@ -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
Expand Down
28 changes: 1 addition & 27 deletions nixos/modules/virtualisation/google-compute-config.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
Expand Down
52 changes: 52 additions & 0 deletions nixos/tests/google-oslogin/default.nix
Original file line number Diff line number Diff line change
@@ -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'");
'';
})

29 changes: 29 additions & 0 deletions nixos/tests/google-oslogin/server.nix
Original file line number Diff line number Diff line change
@@ -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
'';
}
96 changes: 96 additions & 0 deletions nixos/tests/google-oslogin/server.py
Original file line number Diff line number Diff line change
@@ -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()
Loading

0 comments on commit d06f798

Please sign in to comment.