From b389f885cfe4e0cd5da18272da50d9b9fc8934fe Mon Sep 17 00:00:00 2001 From: Louis DeLosSantos Date: Fri, 2 Oct 2020 14:00:56 -0400 Subject: [PATCH] sec: implement jwt signing to ClairV4 (#554) this commit adds jwt signing directly in Quay when contacting ClairV4 Signed-off-by: ldelossa Co-authored-by: ldelossa --- config.py | 4 +++ data/secscan_model/secscan_v4_model.py | 1 + util/config/schema.py | 5 +++ util/secscan/v4/api.py | 45 +++++++++++++++++++++++--- 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/config.py b/config.py index ff199a1b7..b064376bb 100644 --- a/config.py +++ b/config.py @@ -488,6 +488,10 @@ class DefaultConfig(ImmutableConfig): # The issuer name for the security scanner. SECURITY_SCANNER_ISSUER_NAME = "security_scanner" + # A base64 encoded string used to sign JWT(s) on Clair V4 + # requests. If none jwt signing will not occur + SECURITY_SCANNER_V4_PSK = None + # Repository mirror FEATURE_REPO_MIRROR = False diff --git a/data/secscan_model/secscan_v4_model.py b/data/secscan_model/secscan_v4_model.py index bf4473913..5ff8c0eb1 100644 --- a/data/secscan_model/secscan_v4_model.py +++ b/data/secscan_model/secscan_v4_model.py @@ -113,6 +113,7 @@ class V4SecurityScanner(SecurityScannerInterface): endpoint=app.config.get("SECURITY_SCANNER_V4_ENDPOINT"), client=app.config.get("HTTPCLIENT"), blob_url_retriever=BlobURLRetriever(storage, instance_keys, app), + jwt_psk=app.config.get("SECURITY_SCANNER_V4_PSK", None), ) def load_security_information(self, manifest_or_legacy_image, include_vulnerabilities=False): diff --git a/util/config/schema.py b/util/config/schema.py index 539c1b533..d2773cf22 100644 --- a/util/config/schema.py +++ b/util/config/schema.py @@ -703,6 +703,11 @@ CONFIG_SCHEMA = { "description": "The number of seconds between indexing intervals in the security scanner. Defaults to 30.", "x-example": 30, }, + "SECURITY_SCANNER_V4_PSK": { + "type": "string", + "description": "A base64 encoded string used to sign JWT(s) on Clair V4 requests. If 'None' jwt signing will not occur.", + "x-example": "PSK", + }, # Repository mirroring "REPO_MIRROR_INTERVAL": { "type": "number", diff --git a/util/secscan/v4/api.py b/util/secscan/v4/api.py index 83cfbef62..2226a35b8 100644 --- a/util/secscan/v4/api.py +++ b/util/secscan/v4/api.py @@ -2,8 +2,11 @@ import logging import requests import json import os +import jwt +import base64 from collections import namedtuple +from datetime import datetime, timedelta from enum import Enum from abc import ABCMeta, abstractmethod @@ -48,7 +51,7 @@ class SecurityScannerAPIInterface(object): @abstractmethod def state(self): """ - The state endpoint returns a json structure indicating the indexer's internal configuration state. + The state endpoint returns a json structure indicating the indexer's internal configuration state. A client may be interested in this as a signal that manifests may need to be re-indexed. """ pass @@ -56,7 +59,7 @@ class SecurityScannerAPIInterface(object): @abstractmethod def index(self, manifest, layers): """ - By submitting a Manifest object to this endpoint Clair will fetch the layers, + By submitting a Manifest object to this endpoint Clair will fetch the layers, scan each layer's contents, and provide an index of discovered packages, repository and distribution information. Returns a tuple of the `IndexReport` and the indexer state. """ @@ -72,7 +75,7 @@ class SecurityScannerAPIInterface(object): @abstractmethod def vulnerability_report(self, manifest_hash): """ - Given a Manifest's content addressable hash a `VulnerabilityReport` will be created. + Given a Manifest's content addressable hash a `VulnerabilityReport` will be created. The Manifest must have been Indexed first via the Index endpoint. """ pass @@ -114,9 +117,21 @@ actions = { class ClairSecurityScannerAPI(SecurityScannerAPIInterface): - def __init__(self, endpoint, client, blob_url_retriever): + """ + Class implements the SecurityScannerAPIInterface for Clair V4. + + If the jwt_psk value is not None, it must be a base64 encoded string. + The base64 encoded string will be decoded and used to sign JWT(s) for all + Clair V4 requests. + """ + + def __init__(self, endpoint, client, blob_url_retriever, jwt_psk=None): self._client = client self._blob_url_retriever = blob_url_retriever + self.jwt_psk = None + + if jwt_psk is not None: + self.jwt_psk = base64.b64decode(jwt_psk) self.secscan_api_endpoint = endpoint @@ -217,9 +232,15 @@ class ClairSecurityScannerAPI(SecurityScannerAPIInterface): (method, path, body) = action.payload url = urljoin(self.secscan_api_endpoint, path) + headers = {} + if self.jwt_psk: + token = self._sign_jwt() + headers["authorization"] = "{} {}".format("Bearer", token) + logger.debug("generated jwt for security scanner request") + logger.debug("%sing security URL %s", method.upper(), url) try: - resp = self._client.request(method, url, json=body) + resp = self._client.request(method, url, json=body, headers=headers) except requests.exceptions.ConnectionError as ce: logger.exception("Connection error when trying to connect to security scanner endpoint") msg = "Connection error when trying to connect to security scanner endpoint: %s" % str( @@ -240,6 +261,20 @@ class ClairSecurityScannerAPI(SecurityScannerAPIInterface): return resp + def _sign_jwt(self): + """ + Sign and return a jwt. + + If self.jwt_psk is provided a pre-shared key will be used as the signing key. + """ + payload = { + "iss": "quay", + "exp": datetime.utcnow() + timedelta(minutes=5), + } + token = jwt.encode(payload, self.jwt_psk, algorithm="HS256").decode("utf-8") + + return token + def is_valid_response(action, resp): assert action.name in actions.keys()