562492
From 71989367e7a634fdd2af8ef58473975e0ef60464 Mon Sep 17 00:00:00 2001
562492
From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
562492
Date: Sat, 21 Aug 2021 13:53:27 +0200
562492
Subject: [PATCH] Fix home permissions modified by ssh module (SC-338) (#984)
562492
562492
RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
562492
RH-MergeRequest: 29: Fix home permissions modified by ssh module (SC-338) (#984)
562492
RH-Commit: [1/1] c409f2609b1d7e024eba77b55a196a4cafadd1d7 (eesposit/cloud-init)
562492
RH-Bugzilla: 1995840
562492
RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
562492
RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
562492
562492
TESTED: By me and QA
562492
BREW: 39178090
562492
562492
Fix home permissions modified by ssh module (SC-338) (#984)
562492
562492
commit 7d3f5d750f6111c2716143364ea33486df67c927
562492
Author: James Falcon <therealfalcon@gmail.com>
562492
Date:   Fri Aug 20 17:09:49 2021 -0500
562492
562492
    Fix home permissions modified by ssh module (SC-338) (#984)
562492
562492
    Fix home permissions modified by ssh module
562492
562492
    In #956, we updated the file and directory permissions for keys not in
562492
    the user's home directory. We also unintentionally modified the
562492
    permissions within the home directory as well. These should not change,
562492
    and this commit changes that back.
562492
562492
    LP: #1940233
562492
562492
Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
562492
---
562492
 cloudinit/ssh_util.py                         |  35 ++++-
562492
 .../modules/test_ssh_keysfile.py              | 132 +++++++++++++++---
562492
 2 files changed, 146 insertions(+), 21 deletions(-)
562492
562492
diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
562492
index b8a3c8f7..9ccadf09 100644
562492
--- a/cloudinit/ssh_util.py
562492
+++ b/cloudinit/ssh_util.py
562492
@@ -321,23 +321,48 @@ def check_create_path(username, filename, strictmodes):
562492
         home_folder = os.path.dirname(user_pwent.pw_dir)
562492
         for directory in directories:
562492
             parent_folder += "/" + directory
562492
-            if home_folder.startswith(parent_folder):
562492
+
562492
+            # security check, disallow symlinks in the AuthorizedKeysFile path.
562492
+            if os.path.islink(parent_folder):
562492
+                LOG.debug(
562492
+                    "Invalid directory. Symlink exists in path: %s",
562492
+                    parent_folder)
562492
+                return False
562492
+
562492
+            if os.path.isfile(parent_folder):
562492
+                LOG.debug(
562492
+                    "Invalid directory. File exists in path: %s",
562492
+                    parent_folder)
562492
+                return False
562492
+
562492
+            if (home_folder.startswith(parent_folder) or
562492
+                    parent_folder == user_pwent.pw_dir):
562492
                 continue
562492
 
562492
-            if not os.path.isdir(parent_folder):
562492
+            if not os.path.exists(parent_folder):
562492
                 # directory does not exist, and permission so far are good:
562492
                 # create the directory, and make it accessible by everyone
562492
                 # but owned by root, as it might be used by many users.
562492
                 with util.SeLinuxGuard(parent_folder):
562492
-                    os.makedirs(parent_folder, mode=0o755, exist_ok=True)
562492
-                    util.chownbyid(parent_folder, root_pwent.pw_uid,
562492
-                                   root_pwent.pw_gid)
562492
+                    mode = 0o755
562492
+                    uid = root_pwent.pw_uid
562492
+                    gid = root_pwent.pw_gid
562492
+                    if parent_folder.startswith(user_pwent.pw_dir):
562492
+                        mode = 0o700
562492
+                        uid = user_pwent.pw_uid
562492
+                        gid = user_pwent.pw_gid
562492
+                    os.makedirs(parent_folder, mode=mode, exist_ok=True)
562492
+                    util.chownbyid(parent_folder, uid, gid)
562492
 
562492
             permissions = check_permissions(username, parent_folder,
562492
                                             filename, False, strictmodes)
562492
             if not permissions:
562492
                 return False
562492
 
562492
+        if os.path.islink(filename) or os.path.isdir(filename):
562492
+            LOG.debug("%s is not a file!", filename)
562492
+            return False
562492
+
562492
         # check the file
562492
         if not os.path.exists(filename):
562492
             # if file does not exist: we need to create it, since the
562492
diff --git a/tests/integration_tests/modules/test_ssh_keysfile.py b/tests/integration_tests/modules/test_ssh_keysfile.py
562492
index f82d7649..3159feb9 100644
562492
--- a/tests/integration_tests/modules/test_ssh_keysfile.py
562492
+++ b/tests/integration_tests/modules/test_ssh_keysfile.py
562492
@@ -10,10 +10,10 @@ TEST_USER1_KEYS = get_test_rsa_keypair('test1')
562492
 TEST_USER2_KEYS = get_test_rsa_keypair('test2')
562492
 TEST_DEFAULT_KEYS = get_test_rsa_keypair('test3')
562492
 
562492
-USERDATA = """\
562492
+_USERDATA = """\
562492
 #cloud-config
562492
 bootcmd:
562492
- - sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile /etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' /etc/ssh/sshd_config
562492
+ - {bootcmd}
562492
 ssh_authorized_keys:
562492
  - {default}
562492
 users:
562492
@@ -24,27 +24,17 @@ users:
562492
 - name: test_user2
562492
   ssh_authorized_keys:
562492
     - {user2}
562492
-""".format(  # noqa: E501
562492
+""".format(
562492
+    bootcmd='{bootcmd}',
562492
     default=TEST_DEFAULT_KEYS.public_key,
562492
     user1=TEST_USER1_KEYS.public_key,
562492
     user2=TEST_USER2_KEYS.public_key,
562492
 )
562492
 
562492
 
562492
-@pytest.mark.ubuntu
562492
-@pytest.mark.user_data(USERDATA)
562492
-def test_authorized_keys(client: IntegrationInstance):
562492
-    expected_keys = [
562492
-        ('test_user1', '/home/test_user1/.ssh/authorized_keys2',
562492
-         TEST_USER1_KEYS),
562492
-        ('test_user2', '/home/test_user2/.ssh/authorized_keys2',
562492
-         TEST_USER2_KEYS),
562492
-        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2',
562492
-         TEST_DEFAULT_KEYS),
562492
-        ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS),
562492
-    ]
562492
-
562492
+def common_verify(client, expected_keys):
562492
     for user, filename, keys in expected_keys:
562492
+        # Ensure key is in the key file
562492
         contents = client.read_from_file(filename)
562492
         if user in ['ubuntu', 'root']:
562492
             # Our personal public key gets added by pycloudlib
562492
@@ -83,3 +73,113 @@ def test_authorized_keys(client: IntegrationInstance):
562492
                     look_for_keys=False,
562492
                     allow_agent=False,
562492
                 )
562492
+
562492
+        # Ensure we haven't messed with any /home permissions
562492
+        # See LP: #1940233
562492
+        home_dir = '/home/{}'.format(user)
562492
+        home_perms = '755'
562492
+        if user == 'root':
562492
+            home_dir = '/root'
562492
+            home_perms = '700'
562492
+        assert '{} {}'.format(user, home_perms) == client.execute(
562492
+            'stat -c "%U %a" {}'.format(home_dir)
562492
+        )
562492
+        if client.execute("test -d {}/.ssh".format(home_dir)).ok:
562492
+            assert '{} 700'.format(user) == client.execute(
562492
+                'stat -c "%U %a" {}/.ssh'.format(home_dir)
562492
+            )
562492
+        assert '{} 600'.format(user) == client.execute(
562492
+            'stat -c "%U %a" {}'.format(filename)
562492
+        )
562492
+
562492
+        # Also ensure ssh-keygen works as expected
562492
+        client.execute('mkdir {}/.ssh'.format(home_dir))
562492
+        assert client.execute(
562492
+            "ssh-keygen -b 2048 -t rsa -f {}/.ssh/id_rsa -q -N ''".format(
562492
+                home_dir)
562492
+        ).ok
562492
+        assert client.execute('test -f {}/.ssh/id_rsa'.format(home_dir))
562492
+        assert client.execute('test -f {}/.ssh/id_rsa.pub'.format(home_dir))
562492
+
562492
+    assert 'root 755' == client.execute('stat -c "%U %a" /home')
562492
+
562492
+
562492
+DEFAULT_KEYS_USERDATA = _USERDATA.format(bootcmd='""')
562492
+
562492
+
562492
+@pytest.mark.ubuntu
562492
+@pytest.mark.user_data(DEFAULT_KEYS_USERDATA)
562492
+def test_authorized_keys_default(client: IntegrationInstance):
562492
+    expected_keys = [
562492
+        ('test_user1', '/home/test_user1/.ssh/authorized_keys',
562492
+         TEST_USER1_KEYS),
562492
+        ('test_user2', '/home/test_user2/.ssh/authorized_keys',
562492
+         TEST_USER2_KEYS),
562492
+        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys',
562492
+         TEST_DEFAULT_KEYS),
562492
+        ('root', '/root/.ssh/authorized_keys', TEST_DEFAULT_KEYS),
562492
+    ]
562492
+    common_verify(client, expected_keys)
562492
+
562492
+
562492
+AUTHORIZED_KEYS2_USERDATA = _USERDATA.format(bootcmd=(
562492
+    "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile "
562492
+    "/etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' "
562492
+    "/etc/ssh/sshd_config"))
562492
+
562492
+
562492
+@pytest.mark.ubuntu
562492
+@pytest.mark.user_data(AUTHORIZED_KEYS2_USERDATA)
562492
+def test_authorized_keys2(client: IntegrationInstance):
562492
+    expected_keys = [
562492
+        ('test_user1', '/home/test_user1/.ssh/authorized_keys2',
562492
+         TEST_USER1_KEYS),
562492
+        ('test_user2', '/home/test_user2/.ssh/authorized_keys2',
562492
+         TEST_USER2_KEYS),
562492
+        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2',
562492
+         TEST_DEFAULT_KEYS),
562492
+        ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS),
562492
+    ]
562492
+    common_verify(client, expected_keys)
562492
+
562492
+
562492
+NESTED_KEYS_USERDATA = _USERDATA.format(bootcmd=(
562492
+    "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile "
562492
+    "/etc/ssh/authorized_keys %h/foo/bar/ssh/keys;' "
562492
+    "/etc/ssh/sshd_config"))
562492
+
562492
+
562492
+@pytest.mark.ubuntu
562492
+@pytest.mark.user_data(NESTED_KEYS_USERDATA)
562492
+def test_nested_keys(client: IntegrationInstance):
562492
+    expected_keys = [
562492
+        ('test_user1', '/home/test_user1/foo/bar/ssh/keys',
562492
+         TEST_USER1_KEYS),
562492
+        ('test_user2', '/home/test_user2/foo/bar/ssh/keys',
562492
+         TEST_USER2_KEYS),
562492
+        ('ubuntu', '/home/ubuntu/foo/bar/ssh/keys',
562492
+         TEST_DEFAULT_KEYS),
562492
+        ('root', '/root/foo/bar/ssh/keys', TEST_DEFAULT_KEYS),
562492
+    ]
562492
+    common_verify(client, expected_keys)
562492
+
562492
+
562492
+EXTERNAL_KEYS_USERDATA = _USERDATA.format(bootcmd=(
562492
+    "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile "
562492
+    "/etc/ssh/authorized_keys /etc/ssh/authorized_keys/%u/keys;' "
562492
+    "/etc/ssh/sshd_config"))
562492
+
562492
+
562492
+@pytest.mark.ubuntu
562492
+@pytest.mark.user_data(EXTERNAL_KEYS_USERDATA)
562492
+def test_external_keys(client: IntegrationInstance):
562492
+    expected_keys = [
562492
+        ('test_user1', '/etc/ssh/authorized_keys/test_user1/keys',
562492
+         TEST_USER1_KEYS),
562492
+        ('test_user2', '/etc/ssh/authorized_keys/test_user2/keys',
562492
+         TEST_USER2_KEYS),
562492
+        ('ubuntu', '/etc/ssh/authorized_keys/ubuntu/keys',
562492
+         TEST_DEFAULT_KEYS),
562492
+        ('root', '/etc/ssh/authorized_keys/root/keys', TEST_DEFAULT_KEYS),
562492
+    ]
562492
+    common_verify(client, expected_keys)
562492
-- 
562492
2.27.0
562492