-
-
Notifications
You must be signed in to change notification settings - Fork 13.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #51566 from adisbladis/google-oslogin
GCE OSLogin module: init
- Loading branch information
Showing
12 changed files
with
342 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
''; | ||
}; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'"); | ||
''; | ||
}) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
''; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.