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