check_open_ports.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. """A CLI utility for check open ports in the Ray cluster.
  2. See https://www.anyscale.com/blog/update-on-ray-cve-2023-48022-new-verification-tooling-available # noqa: E501
  3. for more details.
  4. """
  5. import json
  6. import subprocess
  7. import urllib
  8. from typing import List, Tuple
  9. import click
  10. import ray
  11. from ray.autoscaler._private.cli_logger import add_click_logging_options, cli_logger
  12. from ray.autoscaler._private.constants import RAY_PROCESSES
  13. from ray.util.annotations import PublicAPI
  14. from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy
  15. import psutil
  16. def _get_ray_ports() -> List[int]:
  17. unique_ports = set()
  18. process_infos = []
  19. for proc in psutil.process_iter(["name", "cmdline"]):
  20. try:
  21. process_infos.append((proc, proc.name(), proc.cmdline()))
  22. except psutil.Error:
  23. pass
  24. for keyword, filter_by_cmd in RAY_PROCESSES:
  25. for candidate in process_infos:
  26. proc, proc_cmd, proc_args = candidate
  27. corpus = proc_cmd if filter_by_cmd else subprocess.list2cmdline(proc_args)
  28. if keyword in corpus:
  29. try:
  30. for connection in proc.connections():
  31. if connection.status == psutil.CONN_LISTEN:
  32. unique_ports.add(connection.laddr.port)
  33. except psutil.AccessDenied:
  34. cli_logger.info(
  35. "Access denied to process connections for process,"
  36. " worker process probably restarted",
  37. proc,
  38. )
  39. return sorted(unique_ports)
  40. def _check_for_open_ports_from_internet(
  41. service_url: str, ports: List[int]
  42. ) -> Tuple[List[int], List[int]]:
  43. request = urllib.request.Request(
  44. method="POST",
  45. url=service_url,
  46. headers={
  47. "Content-Type": "application/json",
  48. "X-Ray-Open-Port-Check": "1",
  49. },
  50. data=json.dumps({"ports": ports}).encode("utf-8"),
  51. )
  52. response = urllib.request.urlopen(request)
  53. if response.status != 200:
  54. raise RuntimeError(
  55. f"Failed to check with Ray Open Port Service: {response.status}"
  56. )
  57. response_body = json.load(response)
  58. publicly_open_ports = response_body.get("open_ports", [])
  59. checked_ports = response_body.get("checked_ports", [])
  60. return publicly_open_ports, checked_ports
  61. def _check_if_exposed_to_internet(
  62. service_url: str,
  63. ) -> Tuple[List[int], List[int]]:
  64. return _check_for_open_ports_from_internet(service_url, _get_ray_ports())
  65. def _check_ray_cluster(
  66. service_url: str,
  67. ) -> List[Tuple[str, Tuple[List[int], List[int]]]]:
  68. ray.init(ignore_reinit_error=True)
  69. @ray.remote(num_cpus=0)
  70. def check(node_id, service_url):
  71. return node_id, _check_if_exposed_to_internet(service_url)
  72. ray_node_ids = [node["NodeID"] for node in ray.nodes() if node["Alive"]]
  73. cli_logger.info(
  74. f"Cluster has {len(ray_node_ids)} node(s)."
  75. " Scheduling tasks on each to check for exposed ports",
  76. )
  77. per_node_tasks = {
  78. node_id: (
  79. check.options(
  80. scheduling_strategy=NodeAffinitySchedulingStrategy(
  81. node_id=node_id, soft=False
  82. )
  83. ).remote(node_id, service_url)
  84. )
  85. for node_id in ray_node_ids
  86. }
  87. results = []
  88. for node_id, per_node_task in per_node_tasks.items():
  89. try:
  90. results.append(ray.get(per_node_task))
  91. except Exception as e:
  92. cli_logger.info(f"Failed to check on node {node_id}: {e}")
  93. return results
  94. @click.command()
  95. @click.option(
  96. "--yes", "-y", is_flag=True, default=False, help="Don't ask for confirmation."
  97. )
  98. @click.option(
  99. "--service-url",
  100. required=False,
  101. type=str,
  102. default="https://ray-open-port-checker.uc.r.appspot.com/open-port-check",
  103. help="The url of service that checks whether submitted ports are open.",
  104. )
  105. @add_click_logging_options
  106. @PublicAPI
  107. def check_open_ports(yes, service_url):
  108. """Check open ports in the local Ray cluster."""
  109. if not cli_logger.confirm(
  110. yes=yes,
  111. msg=(
  112. "Do you want to check the local Ray cluster"
  113. " for any nodes with ports accessible to the internet?"
  114. ),
  115. _default=True,
  116. ):
  117. cli_logger.info("Exiting without checking as instructed")
  118. return
  119. cluster_open_ports = _check_ray_cluster(service_url)
  120. public_nodes = []
  121. for node_id, (open_ports, checked_ports) in cluster_open_ports:
  122. if open_ports:
  123. cli_logger.info(
  124. f"[🛑] open ports detected open_ports={open_ports!r} node={node_id!r}"
  125. )
  126. public_nodes.append((node_id, open_ports, checked_ports))
  127. else:
  128. cli_logger.info(
  129. f"[🟢] No open ports detected "
  130. f"checked_ports={checked_ports!r} node={node_id!r}"
  131. )
  132. cli_logger.info("Check complete, results:")
  133. if public_nodes:
  134. cli_logger.info(
  135. """
  136. [🛑] An server on the internet was able to open a connection to one of this Ray
  137. cluster's public IP on one of Ray's internal ports. If this is not a false
  138. positive, this is an extremely unsafe configuration for Ray to be running in.
  139. Ray is not meant to be exposed to untrusted clients and will allow them to run
  140. arbitrary code on your machine.
  141. You should take immediate action to validate this result and if confirmed shut
  142. down your Ray cluster immediately and take appropriate action to remediate its
  143. exposure. Anything either running on this Ray cluster or that this cluster has
  144. had access to could be at risk.
  145. For guidance on how to operate Ray safely, please review [Ray's security
  146. documentation](https://docs.ray.io/en/latest/ray-security/index.html).
  147. """.strip()
  148. )
  149. else:
  150. cli_logger.info("[🟢] No open ports detected from any Ray nodes")