Skip to content

Commit

Permalink
Improved SSH host key verification
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert Szefler committed Feb 28, 2024
1 parent c9a0a0a commit dc93da1
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,57 @@ use in :ref:`customPlaybooks<customPlaybooks>`.

External actions are loaded using the ``playbookRepos`` Helm value, with either HTTPs or SSH.

If your repository is not in ``github.com`` or ``bitbucket.org`` (default verified domains), please add your repository domains:
If you are going to be using an external repository via HTTPS, you just need to configure
correct read access credentials (see below). When connecting via SSH, however, there is an
additional requirement to verify the remote host's identity on the client side, as SSH
generally does not provide any method of doing that automatically (in contrast with HTTPS,
which relies on the well established cryptographic infrastructure of certificates).

In order to streamline the process of SSH host key verification, Robusta ships with verified
host keys for the following popular Git providers:

* github.com
* gitlab.com
* bitbucket.org
* ssh.dev.azure.com

If you are using a Git service outside of that list, you should add its SSH host keys in Robusta
configuration. This is done via the `CUSTOM_SSH_HOST_KEYS` environment variable with the list
of keys separated by newlines:

.. code-block:: yaml
runner:
additional_env_vars:
- name: GIT_REPOS_VERIFIED_HOSTS
value: "ssh.dev.azure.com gitlab.com"
- name: CUSTOM_SSH_HOST_KEYS
- value: |
key-1
key-2
key-3
Another option to automate host key verification it the `GIT_REPOS_VERIFIED_HOSTS` environment
variable.

.. warning::

**DANGER ZONE**

Using the `GIT_REPOS_VERIFIED_HOSTS` variable is generally not recommended due to
security issues. Each host added this way will be automatically trusted *without*
an actual host key verification, potentially allowing man-in-the-middle attacks with
catastrophic implications. For more information, see
`here <https://www.ssh.com/academy/attack/man-in-the-middle>`_.

Please make sure you know what you are doing before using this functionality.

An example of using that configuration option:

.. code-block:: yaml
runner:
additional_env_vars:
- name: GIT_REPOS_VERIFIED_HOSTS
value: "ssh.yourhost.com ssh.anotherhost.com"
Loading Actions from Public Git Repo
------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions src/robusta/core/model/env_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def load_bool(env_var, default: bool):
DEFAULT_PLAYBOOKS_PIP_INSTALL = load_bool("DEFAULT_PLAYBOOKS_PIP_INSTALL", False)

CUSTOM_PLAYBOOKS_ROOT = os.path.join(PLAYBOOKS_ROOT, "storage")
CUSTOM_SSH_HOST_KEYS = os.environ.get("CUSTOM_SSH_HOST_KEYS", "")

PLAYBOOKS_CONFIG_FILE_PATH = os.environ.get("PLAYBOOKS_CONFIG_FILE_PATH")

Expand Down
28 changes: 21 additions & 7 deletions src/robusta/integrations/git/git_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing import Dict, List

from robusta.core.model.env_vars import GIT_MAX_RETRIES
from robusta.integrations.git.well_known_hosts import WELL_KNOWN_HOST_KEYS

GIT_DIR_NAME = "robusta-git"
REPO_LOCAL_BASE_DIR = os.path.abspath(os.path.join(os.environ.get("REPO_LOCAL_BASE_DIR", "/app"), GIT_DIR_NAME))
Expand All @@ -26,6 +27,20 @@ class GitRepoManager:
manager_lock = threading.Lock()
repo_map = defaultdict(None)

@staticmethod
def setup_host_keys(custom_host_keys: List[str]):
if not os.path.exists(SSH_ROOT_DIR):
os.mkdir(SSH_ROOT_DIR)
with open(f"{SSH_ROOT_DIR}/known_hosts", "w") as f:
for key in WELL_KNOWN_HOST_KEYS + custom_host_keys:
key = key.strip()
if key:
logging.debug(f"Adding a key to known_hosts: {key}")
f.write(key)

if GIT_REPOS_VERIFIED_HOSTS:
os.system(f"ssh-keyscan -H {GIT_REPOS_VERIFIED_HOSTS} >> {SSH_ROOT_DIR}/known_hosts")

@staticmethod
def get_git_repo(git_repo_url: str, git_key: str):
with GitRepoManager.manager_lock:
Expand All @@ -41,8 +56,8 @@ def remove_git_repo(git_repo_url):
with GitRepoManager.manager_lock:
del GitRepoManager.repo_map[git_repo_url]

@staticmethod
def clear_git_repos():
@classmethod
def clear_git_repos(cls):
with GitRepoManager.manager_lock:
GitRepoManager.repo_map.clear()

Expand All @@ -60,10 +75,12 @@ def __init__(self, git_repo_url: str, git_key: str, git_branch: str = None):
self.git_repo_url = git_repo_url
self.env = os.environ.copy()
self.git_branch = git_branch
ssh_key_option = ""

if git_key: # Add ssh key for non-public repositories
key_file_name = self.init_key(git_key)
ssh_key_option = f"-i {key_file_name}"
else:
ssh_key_option = ""

self.env["GIT_SSH_COMMAND"] = f"ssh {ssh_key_option} -o IdentitiesOnly=yes"
self.repo_lock = threading.RLock()
Expand All @@ -82,11 +99,8 @@ def init_key(self, git_key):
git_key = git_key + "\n"

with open(key_file_name, "w") as key_file:
os.chmod(key_file_name, 0o400)
key_file.write(textwrap.dedent(f"{git_key}"))
os.chmod(key_file_name, 0o400)
if not os.path.exists(SSH_ROOT_DIR):
os.mkdir(SSH_ROOT_DIR)
os.system(f"ssh-keyscan -H github.com bitbucket.org {GIT_REPOS_VERIFIED_HOSTS} >> {SSH_ROOT_DIR}/known_hosts")
return key_file_name

@staticmethod
Expand Down
16 changes: 16 additions & 0 deletions src/robusta/integrations/git/well_known_hosts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
WELL_KNOWN_HOST_KEYS = [
# github.com
"|1|8/btn2nIvP08st0rffVzfBw/u7o=|7jWoObKOrX9Q68fkdIfHnvw4/xc= ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=",
"|1|5pBsB/qQfvYG5u33Fe5wQw383XI=|8gDX+h6v0hpMrQrQIlKGVDjHQ98= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=",
"|1|a3CVdE3rzmO3nD3HMdYtX+ZS8yE=|QoMmPJiUV67E51Qf4jyceMAGHzs= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl",
# gitlab.com
"|1|LiZMgMrzgvu/B5SOn95fLbtM0DA=|XvW22X6+G9QDtY61+6w5ns5xEbo= ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9",
"|1|YbnG/64qNFA2Cf9qt5BD2Aj4rO0=|kf1ipE8aTntrFxyHLAxZ/BQy7iU= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY="
"|1|g/BQ/0dlrf4oABFby7FWFPLvpiM=|WWrhkEzfupIH172XUsOlove4MPc= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf",
# bitbucket.org
"|1|XoBV2bENlv6QLsqfAmAOW68cM9A=|kNOmO8JCtG47o4n967vPlWRxzJI= ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M=",
"|1|aAanWvP59AXJG63mKQRaW3JrUGc=|pBIqK8MnUUisy4Cq5g+I+uIR6pw= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPIQmuzMBuKdWeF4+a2sjSSpBK0iqitSQ+5BM9KhpexuGt20JpTVM7u5BDZngncgrqDMbWdxMWWOGtZ9UgbqgZE=",
"|1|8FbblX02OSTs6bOHkKv4ZsLBvjE=|9H2xv53vfCSHX7OQdAc1DnQzzjo= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO",
# ssh.dev.azure.com
"|1|HlUhUTA+Je1obG7hPFVLGOhfWD0=|A+mrcbaWzLwVwxBiZaqrcHjTGhE= ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H",
]
2 changes: 2 additions & 0 deletions src/robusta/runner/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from robusta.cli.utils import get_package_name
from robusta.core.model.env_vars import (
CUSTOM_SSH_HOST_KEYS,
CUSTOM_PLAYBOOKS_ROOT,
DEFAULT_PLAYBOOKS_PIP_INSTALL,
DEFAULT_PLAYBOOKS_ROOT,
Expand Down Expand Up @@ -105,6 +106,7 @@ def __load_playbooks_repos(
playbooks_repos: Dict[str, PlaybookRepo],
):
playbook_packages = []
GitRepoManager.setup_host_keys(CUSTOM_SSH_HOST_KEYS.split("\n"))
for playbook_package, playbooks_repo in playbooks_repos.items():
try:
if playbooks_repo.pip_install: # skip playbooks that are already in site-packages
Expand Down

0 comments on commit dc93da1

Please sign in to comment.