b19c921a82
Resolves: rhbz#2082989
357 lines
15 KiB
Diff
357 lines
15 KiB
Diff
From 16d3a31145e3d0001e6c6621adb6dbaf831cb03f Mon Sep 17 00:00:00 2001
|
|
From: Daiki Ueno <dueno@redhat.com>
|
|
Date: Mon, 21 Mar 2022 13:08:29 +0100
|
|
Subject: [PATCH 4/5] cloud_verifier: Support /notifications/revocation REST
|
|
API
|
|
|
|
Signed-off-by: Daiki Ueno <dueno@redhat.com>
|
|
---
|
|
keylime.conf | 22 ++++--
|
|
keylime/cloud_verifier_common.py | 16 +----
|
|
keylime/cloud_verifier_tornado.py | 108 +++++++++++++++++++++++-------
|
|
keylime/revocation_notifier.py | 18 ++++-
|
|
4 files changed, 119 insertions(+), 45 deletions(-)
|
|
|
|
diff --git a/keylime.conf b/keylime.conf
|
|
index fbd1119..c393e22 100644
|
|
--- a/keylime.conf
|
|
+++ b/keylime.conf
|
|
@@ -269,9 +269,21 @@ max_retries = 5
|
|
# will done as fast as possible. Floating point values accepted here.
|
|
quote_interval = 2
|
|
|
|
-# Whether to turn on the zero mq based revocation notifier system.
|
|
-# Currently this only works if you are using keylime-CA.
|
|
-revocation_notifier = True
|
|
+# Enable listed revocation notification methods.
|
|
+#
|
|
+# Available methods are:
|
|
+#
|
|
+# "zeromq": Enable the ZeroMQ based revocation notification method;
|
|
+# revocation_notifier_ip and revocation_notifier_port options must be
|
|
+# set. Currently this only works if you are using keylime-CA.
|
|
+#
|
|
+# "webhook": Send notification via webhook. The endpoint URL must be
|
|
+# configured with webhook_url option. This can be used to notify other
|
|
+# systems that do not have a Keylime agent running.
|
|
+#
|
|
+# "agent": Deliver notification directly to the agent via the REST
|
|
+# protocol.
|
|
+revocation_notifiers = zeromq
|
|
|
|
# The binding address and port of the revocation notifier service.
|
|
# If the 'revocation_notifier' option is set to "true", then the verifier
|
|
@@ -279,10 +291,6 @@ revocation_notifier = True
|
|
revocation_notifier_ip = 127.0.0.1
|
|
revocation_notifier_port = 8992
|
|
|
|
-# Enable revocation notifications via webhook. This can be used to notify other
|
|
-# systems that do not have a Keylime agent running.
|
|
-revocation_notifier_webhook = False
|
|
-
|
|
# Webhook url for revocation notifications.
|
|
webhook_url = ''
|
|
|
|
diff --git a/keylime/cloud_verifier_common.py b/keylime/cloud_verifier_common.py
|
|
index 52d6908..ab00768 100644
|
|
--- a/keylime/cloud_verifier_common.py
|
|
+++ b/keylime/cloud_verifier_common.py
|
|
@@ -7,7 +7,7 @@ import ast
|
|
import base64
|
|
import time
|
|
|
|
-from keylime import config, crypto, json, keylime_logging, revocation_notifier
|
|
+from keylime import config, crypto, json, keylime_logging
|
|
from keylime.agentstates import AgentAttestStates
|
|
from keylime.common import algorithms, validators
|
|
from keylime.failure import Component, Failure
|
|
@@ -268,14 +268,7 @@ def process_get_status(agent):
|
|
|
|
|
|
# sign a message with revocation key. telling of verification problem
|
|
-
|
|
-
|
|
-def notify_error(agent, msgtype="revocation", event=None):
|
|
- send_mq = config.getboolean("cloud_verifier", "revocation_notifier")
|
|
- send_webhook = config.getboolean("cloud_verifier", "revocation_notifier_webhook", fallback=False)
|
|
- if not (send_mq or send_webhook):
|
|
- return
|
|
-
|
|
+def prepare_error(agent, msgtype="revocation", event=None):
|
|
# prepare the revocation message:
|
|
revocation = {
|
|
"type": msgtype,
|
|
@@ -300,10 +293,7 @@ def notify_error(agent, msgtype="revocation", event=None):
|
|
|
|
else:
|
|
tosend["signature"] = "none"
|
|
- if send_mq:
|
|
- revocation_notifier.notify(tosend)
|
|
- if send_webhook:
|
|
- revocation_notifier.notify_webhook(tosend)
|
|
+ return tosend
|
|
|
|
|
|
def validate_agent_data(agent_data):
|
|
diff --git a/keylime/cloud_verifier_tornado.py b/keylime/cloud_verifier_tornado.py
|
|
index 60abf37..a8c08d2 100644
|
|
--- a/keylime/cloud_verifier_tornado.py
|
|
+++ b/keylime/cloud_verifier_tornado.py
|
|
@@ -9,6 +9,7 @@ import os
|
|
import signal
|
|
import sys
|
|
import traceback
|
|
+from concurrent.futures import ThreadPoolExecutor
|
|
from multiprocessing import Process
|
|
|
|
import tornado.ioloop
|
|
@@ -35,6 +36,9 @@ from keylime.failure import MAX_SEVERITY_LABEL, Component, Failure
|
|
|
|
logger = keylime_logging.init_logging("cloudverifier")
|
|
|
|
+# mTLS configuration to connect to the agent
|
|
+mtls_options = None
|
|
+
|
|
|
|
try:
|
|
engine = DBEngineManager().make_engine("cloud_verifier")
|
|
@@ -232,11 +236,6 @@ class VersionHandler(BaseHandler):
|
|
|
|
|
|
class AgentsHandler(BaseHandler):
|
|
- mtls_options = None # Stores the cert, key and password used by the verifier for mTLS connections
|
|
-
|
|
- def initialize(self, mtls_options):
|
|
- self.mtls_options = mtls_options
|
|
-
|
|
def head(self):
|
|
"""HEAD not supported"""
|
|
web_util.echo_json_response(self, 405, "HEAD not supported")
|
|
@@ -496,9 +495,7 @@ class AgentsHandler(BaseHandler):
|
|
mtls_cert = agent_data["mtls_cert"]
|
|
agent_data["ssl_context"] = None
|
|
if agent_mtls_cert_enabled and mtls_cert:
|
|
- agent_data["ssl_context"] = web_util.generate_agent_mtls_context(
|
|
- mtls_cert, self.mtls_options
|
|
- )
|
|
+ agent_data["ssl_context"] = web_util.generate_agent_mtls_context(mtls_cert, mtls_options)
|
|
|
|
if agent_data["ssl_context"] is None:
|
|
logger.warning("Connecting to agent without mTLS: %s", agent_id)
|
|
@@ -566,7 +563,7 @@ class AgentsHandler(BaseHandler):
|
|
if not isinstance(agent, dict):
|
|
agent = _from_db_obj(agent)
|
|
if agent["mtls_cert"]:
|
|
- agent["ssl_context"] = web_util.generate_agent_mtls_context(agent["mtls_cert"], self.mtls_options)
|
|
+ agent["ssl_context"] = web_util.generate_agent_mtls_context(agent["mtls_cert"], mtls_options)
|
|
agent["operational_state"] = states.START
|
|
asyncio.ensure_future(process_agent(agent, states.GET_QUOTE))
|
|
web_util.echo_json_response(self, 200, "Success")
|
|
@@ -860,6 +857,70 @@ async def invoke_provide_v(agent):
|
|
asyncio.ensure_future(process_agent(agent, states.GET_QUOTE))
|
|
|
|
|
|
+async def invoke_notify_error(agent, tosend):
|
|
+ if agent is None:
|
|
+ logger.warning("Agent deleted while being processed")
|
|
+ return
|
|
+ kwargs = {
|
|
+ "data": tosend,
|
|
+ }
|
|
+ if agent["ssl_context"]:
|
|
+ kwargs["context"] = agent["ssl_context"]
|
|
+ res = tornado_requests.request(
|
|
+ "POST",
|
|
+ f"http://{agent['ip']}:{agent['port']}/v{agent['supported_version']}/notifications/revocation",
|
|
+ **kwargs,
|
|
+ )
|
|
+ response = await res
|
|
+
|
|
+ if response is None:
|
|
+ logger.warning(
|
|
+ "Empty Notify Revocation response from cloud agent %s",
|
|
+ agent["agent_id"],
|
|
+ )
|
|
+ elif response.status_code != 200:
|
|
+ logger.warning(
|
|
+ "Unexpected Notify Revocation response error for cloud agent %s, Error: %s",
|
|
+ agent["agent_id"],
|
|
+ response.status_code,
|
|
+ )
|
|
+
|
|
+
|
|
+async def notify_error(agent, msgtype="revocation", event=None):
|
|
+ notifiers = revocation_notifier.get_notifiers()
|
|
+ if len(notifiers) == 0:
|
|
+ return
|
|
+
|
|
+ tosend = cloud_verifier_common.prepare_error(agent, msgtype, event)
|
|
+ if "webhook" in notifiers:
|
|
+ revocation_notifier.notify_webhook(tosend)
|
|
+ if "zeromq" in notifiers:
|
|
+ revocation_notifier.notify(tosend)
|
|
+ if "agent" in notifiers:
|
|
+ verifier_id = config.get(
|
|
+ "cloud_verifier", "cloudverifier_id", fallback=cloud_verifier_common.DEFAULT_VERIFIER_ID
|
|
+ )
|
|
+ session = get_session()
|
|
+ agents = session.query(VerfierMain).filter_by(verifier_id=verifier_id).all()
|
|
+ futures = []
|
|
+ loop = asyncio.get_event_loop()
|
|
+ # Notify all agents asynchronously through a thread pool
|
|
+ with ThreadPoolExecutor() as pool:
|
|
+ for agent_db_obj in agents:
|
|
+ if agent_db_obj.agent_id != agent["agent_id"]:
|
|
+ agent = _from_db_obj(agent_db_obj)
|
|
+ if agent["mtls_cert"]:
|
|
+ agent["ssl_context"] = web_util.generate_agent_mtls_context(agent["mtls_cert"], mtls_options)
|
|
+ func = functools.partial(invoke_notify_error, agent, tosend)
|
|
+ futures.append(await loop.run_in_executor(pool, func))
|
|
+ # Wait for all tasks complete in 60 seconds
|
|
+ try:
|
|
+ for f in asyncio.as_completed(futures, timeout=60):
|
|
+ await f
|
|
+ except asyncio.TimeoutError as e:
|
|
+ logger.error("Timeout during notifying error to agents: %s", e)
|
|
+
|
|
+
|
|
async def process_agent(agent, new_operational_state, failure=Failure(Component.INTERNAL, ["verifier"])):
|
|
# Convert to dict if the agent arg is a db object
|
|
if not isinstance(agent, dict):
|
|
@@ -900,7 +961,7 @@ async def process_agent(agent, new_operational_state, failure=Failure(Component.
|
|
|
|
# issue notification for invalid quotes
|
|
if new_operational_state == states.INVALID_QUOTE:
|
|
- cloud_verifier_common.notify_error(agent, event=failure.highest_severity_event)
|
|
+ await notify_error(agent, event=failure.highest_severity_event)
|
|
|
|
# When the failure is irrecoverable we stop polling the agent
|
|
if not failure.recoverable or failure.highest_severity == MAX_SEVERITY_LABEL:
|
|
@@ -975,9 +1036,7 @@ async def process_agent(agent, new_operational_state, failure=Failure(Component.
|
|
)
|
|
failure.add_event("not_reachable", "agent was not reachable from verifier", False)
|
|
if agent["first_verified"]: # only notify on previously good agents
|
|
- cloud_verifier_common.notify_error(
|
|
- agent, msgtype="comm_error", event=failure.highest_severity_event
|
|
- )
|
|
+ await notify_error(agent, msgtype="comm_error", event=failure.highest_severity_event)
|
|
else:
|
|
logger.debug("Communication error for new agent. No notification will be sent")
|
|
await process_agent(agent, states.FAILED, failure)
|
|
@@ -1004,7 +1063,7 @@ async def process_agent(agent, new_operational_state, failure=Failure(Component.
|
|
maxr,
|
|
)
|
|
failure.add_event("not_reachable_v", "agent was not reachable to provide V", False)
|
|
- cloud_verifier_common.notify_error(agent, msgtype="comm_error", event=failure.highest_severity_event)
|
|
+ await notify_error(agent, msgtype="comm_error", event=failure.highest_severity_event)
|
|
await process_agent(agent, states.FAILED, failure)
|
|
else:
|
|
agent["operational_state"] = states.PROVIDE_V
|
|
@@ -1031,7 +1090,7 @@ async def process_agent(agent, new_operational_state, failure=Failure(Component.
|
|
await process_agent(agent, states.FAILED, failure)
|
|
|
|
|
|
-async def activate_agents(verifier_id, verifier_ip, verifier_port, mtls_options):
|
|
+async def activate_agents(verifier_id, verifier_ip, verifier_port):
|
|
session = get_session()
|
|
aas = get_AgentAttestStates()
|
|
try:
|
|
@@ -1100,7 +1159,7 @@ def main():
|
|
# print out API versions we support
|
|
keylime_api_version.log_api_versions(logger)
|
|
|
|
- context, mtls_options = web_util.init_mtls(logger=logger)
|
|
+ context, server_mtls_options = web_util.init_mtls(logger=logger)
|
|
|
|
# Check for user defined CA to connect to agent
|
|
agent_mtls_cert = config.get("cloud_verifier", "agent_mtls_cert", fallback=None)
|
|
@@ -1108,12 +1167,15 @@ def main():
|
|
agent_mtls_private_key_pw = config.get("cloud_verifier", "agent_mtls_private_key_pw", fallback=None)
|
|
|
|
# Only set custom options if the cert should not be the same as used by the verifier
|
|
- if agent_mtls_cert != "CV":
|
|
+ global mtls_options
|
|
+ if agent_mtls_cert == "CV":
|
|
+ mtls_options = server_mtls_options
|
|
+ else:
|
|
mtls_options = (agent_mtls_cert, agent_mtls_private_key, agent_mtls_private_key_pw)
|
|
|
|
app = tornado.web.Application(
|
|
[
|
|
- (r"/v?[0-9]+(?:\.[0-9]+)?/agents/.*", AgentsHandler, {"mtls_options": mtls_options}),
|
|
+ (r"/v?[0-9]+(?:\.[0-9]+)?/agents/.*", AgentsHandler),
|
|
(r"/v?[0-9]+(?:\.[0-9]+)?/allowlists/.*", AllowlistHandler),
|
|
(r"/versions?", VersionHandler),
|
|
(r".*", MainHandler),
|
|
@@ -1149,17 +1211,17 @@ def main():
|
|
server.start()
|
|
if task_id == 0:
|
|
# Reactivate agents
|
|
- asyncio.ensure_future(
|
|
- activate_agents(cloudverifier_id, cloudverifier_host, cloudverifier_port, mtls_options)
|
|
- )
|
|
+ asyncio.ensure_future(activate_agents(cloudverifier_id, cloudverifier_host, cloudverifier_port))
|
|
tornado.ioloop.IOLoop.current().start()
|
|
logger.debug("Server %s stopped.", task_id)
|
|
sys.exit(0)
|
|
|
|
processes = []
|
|
|
|
+ run_revocation_notifier = "zeromq" in revocation_notifier.get_notifiers()
|
|
+
|
|
def sig_handler(*_):
|
|
- if config.getboolean("cloud_verifier", "revocation_notifier"):
|
|
+ if run_revocation_notifier:
|
|
revocation_notifier.stop_broker()
|
|
for p in processes:
|
|
p.join()
|
|
@@ -1167,7 +1229,7 @@ def main():
|
|
|
|
signal.signal(signal.SIGINT, sig_handler)
|
|
signal.signal(signal.SIGTERM, sig_handler)
|
|
- if config.getboolean("cloud_verifier", "revocation_notifier"):
|
|
+ if run_revocation_notifier:
|
|
logger.info(
|
|
"Starting service for revocation notifications on port %s",
|
|
config.getint("cloud_verifier", "revocation_notifier_port"),
|
|
diff --git a/keylime/revocation_notifier.py b/keylime/revocation_notifier.py
|
|
index 7cfe0e5..0628a64 100644
|
|
--- a/keylime/revocation_notifier.py
|
|
+++ b/keylime/revocation_notifier.py
|
|
@@ -22,8 +22,22 @@ broker_proc: Optional[Process] = None
|
|
_SOCKET_PATH = "/var/run/keylime/keylime.verifier.ipc"
|
|
|
|
|
|
+# return the revocation notification methods for cloud verifier
|
|
+def get_notifiers():
|
|
+ notifiers = set(config.get("cloud_verifier", "revocation_notifiers", fallback="").split(","))
|
|
+ if ("zeromq" not in notifiers) and config.getboolean("cloud_verifier", "revocation_notifier", fallback=False):
|
|
+ logger.warning("Warning: 'revocation_notifier' option is deprecated; use 'revocation_notifiers'")
|
|
+ notifiers.add("zeromq")
|
|
+ if ("webhook" not in notifiers) and config.getboolean(
|
|
+ "cloud_verifier", "revocation_notifier_webhook", fallback=False
|
|
+ ):
|
|
+ logger.warning("Warning: 'revocation_notifier_webhook' option is deprecated; use 'revocation_notifiers'")
|
|
+ notifiers.add("webhook")
|
|
+ return notifiers.intersection({"zeromq", "webhook", "agent"})
|
|
+
|
|
+
|
|
def start_broker():
|
|
- assert config.getboolean("cloud_verifier", "revocation_notifier")
|
|
+ assert "zeromq" in get_notifiers()
|
|
try:
|
|
import zmq # pylint: disable=import-outside-toplevel
|
|
except ImportError as error:
|
|
@@ -78,7 +92,7 @@ def stop_broker():
|
|
|
|
|
|
def notify(tosend):
|
|
- assert config.getboolean("cloud_verifier", "revocation_notifier")
|
|
+ assert "zeromq" in get_notifiers()
|
|
try:
|
|
import zmq # pylint: disable=import-outside-toplevel
|
|
except ImportError as error:
|
|
--
|
|
2.35.1
|
|
|