A long-lived cert was created


Description

An unusually long-lived Teleport certificate was created

Query · python

from datetime import datetime, timedelta
from typing import Dict, Tuple

from panther_base_helpers import (
    golang_nanotime_to_python_datetime,
    panther_nanotime_to_python_datetime,
)

PANTHER_TIME_FORMAT = r"%Y-%m-%d %H:%M:%S.%f"
# Tune this to be some Greatest Common Denominator of session TTLs for your
# environment
MAXIMUM_NORMAL_VALIDITY_INTERVAL = timedelta(hours=12)
# To allow some time in between when a request is submitted and authorized
# vs when the certificate actually gets generated. In practice, this is much
# less than 5 seconds.
ISSUANCE_GRACE_PERIOD = timedelta(seconds=5)

# You can audit your logs in Panther to try and understand your role/validity
# patterns from a known-good period of access.
# A query example:
# ```sql
#  SELECT
#     cluster_name,
#     identity:roles,
#     DATEDIFF('HOUR', time, identity:expires) AS validity
#  FROM
#     panther_logs.public.gravitational_teleportaudit
#  WHERE
#     p_occurs_between('2023-09-01 00:00:00','2023-10-06 21:00:00Z')
#     AND event = 'cert.create'
#  GROUP BY cluster_name, identity:roles, validity
#  ORDER BY validity DESC
# ```

# A dictionary of:
#  cluster names: to a dictionary of:
#     role names: mapping to a tuple of:
#        ( maximum usual validity, expiration datetime for this rule )
CLUSTER_ROLE_MAX_VALIDITIES: Dict[str, Dict[str, Tuple[timedelta, datetime]]] = {
    # "teleport.example.com": {
    #     "example_role": (timedelta(hours=720), datetime(2023, 12, 01, 01, 02, 03)),
    #     "other_example_role": (timedelta(hours=720), datetime.max),
    # },
}


def rule(event):
    if not event.get("event") == "cert.create":
        return False
    max_validity = MAXIMUM_NORMAL_VALIDITY_INTERVAL + ISSUANCE_GRACE_PERIOD
    for role in event.deep_get("identity", "roles", default=[]):
        validity, expiration = CLUSTER_ROLE_MAX_VALIDITIES.get(event.get("cluster_name"), {}).get(
            role, (None, None)
        )
        if validity and expiration:
            # Ignore exceptions that have passed their expiry date
            if datetime.utcnow() < expiration:
                max_validity = max(max_validity, validity)
    return validity_interval(event) > max_validity


def validity_interval(event):
    event_time = panther_nanotime_to_python_datetime(event.get("time"))
    expires = golang_nanotime_to_python_datetime(
        event.deep_get("identity", "expires", default=None)
    )
    if not event_time and expires:
        return False
    interval = expires - event_time
    return interval


def title(event):
    identity = event.deep_get("identity", "user", default="<Cert with no User!?>")
    return (
        f"A Certificate for [{identity}] "
        f"on [{event.get('cluster_name', '<UNKNOWN_CLUSTER>')}] "
        f"has been issued for an unusually long time: {validity_interval(event)!r} "
    )

Analyst notes

Teleport certificates are usually issued for a short period of time. Alert if long-lived certificates were created.

Raw source A long-lived cert was created · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
Filename: teleport_long_lived_certs.py
RuleID: Teleport.LongLivedCerts
DisplayName: A long-lived cert was created
Enabled: true
LogTypes:
  - Gravitational.TeleportAudit
Tags:
  - Teleport
Severity: Medium
Description: An unusually long-lived Teleport certificate was created
DedupPeriodMinutes: 60
Reports:
  MITRE ATT&CK:
    - TA0003:T1098
Reference: https://goteleport.com/docs/management/admin/
Runbook: >
  Teleport certificates are usually issued for a short period of time. Alert if long-lived certificates were created.
SummaryAttributes:
  - event
  - code
  - time
  - identity
Tests:
  - Name: A certificate was created for the default period of 1 hour
    ExpectedResult: false
    Log:
      {
        "cert_type": "user",
        "cluster_name": "teleport.example.com",
        "code": "TC000I",
        "ei": 0,
        "event": "cert.create",
        "time": "2023-09-17 21:00:00.000000",
        "identity":
          {
            "disallow_reissue": true,
            "expires": "2023-09-17T22:00:00.444444428Z",
            "impersonator": "bot-application",
            "kubernetes_cluster": "staging",
            "kubernetes_groups": ["application"],
            "logins":
              [
                "-teleport-nologin-88888888-4444-4444-4444-222222222222",
                "-teleport-internal-join",
              ],
            "prev_identity_expires": "0001-01-01T00:00:00Z",
            "roles": ["application"],
            "route_to_cluster": "teleport.example.com",
            "teleport_cluster": "teleport.example.com",
            "traits": {},
            "user": "bot-application",
          },
        "uid": "88888888-4444-4444-4444-222222222222",
      }
  - Name: A certificate was created for longer than the default period of 1 hour
    ExpectedResult: true
    Log:
      {
        "cert_type": "user",
        "cluster_name": "teleport.example.com",
        "code": "TC000I",
        "ei": 0,
        "event": "cert.create",
        "time": "2023-09-17 21:00:00.000000",
        "identity":
          {
            "disallow_reissue": true,
            "expires": "2043-09-17T22:00:00.444444428Z",
            "impersonator": "bot-application",
            "kubernetes_cluster": "staging",
            "kubernetes_groups": ["application"],
            "logins":
              [
                "-teleport-nologin-88888888-4444-4444-4444-222222222222",
                "-teleport-internal-join",
              ],
            "prev_identity_expires": "0001-01-01T00:00:00Z",
            "roles": ["application"],
            "route_to_cluster": "teleport.example.com",
            "teleport_cluster": "teleport.example.com",
            "traits": {},
            "user": "bot-application",
          },
        "uid": "88888888-4444-4444-4444-222222222222",
      }


# ------ paired body: teleport_long_lived_certs.py ------

from datetime import datetime, timedelta
from typing import Dict, Tuple

from panther_base_helpers import (
    golang_nanotime_to_python_datetime,
    panther_nanotime_to_python_datetime,
)

PANTHER_TIME_FORMAT = r"%Y-%m-%d %H:%M:%S.%f"
# Tune this to be some Greatest Common Denominator of session TTLs for your
# environment
MAXIMUM_NORMAL_VALIDITY_INTERVAL = timedelta(hours=12)
# To allow some time in between when a request is submitted and authorized
# vs when the certificate actually gets generated. In practice, this is much
# less than 5 seconds.
ISSUANCE_GRACE_PERIOD = timedelta(seconds=5)

# You can audit your logs in Panther to try and understand your role/validity
# patterns from a known-good period of access.
# A query example:
# ```sql
#  SELECT
#     cluster_name,
#     identity:roles,
#     DATEDIFF('HOUR', time, identity:expires) AS validity
#  FROM
#     panther_logs.public.gravitational_teleportaudit
#  WHERE
#     p_occurs_between('2023-09-01 00:00:00','2023-10-06 21:00:00Z')
#     AND event = 'cert.create'
#  GROUP BY cluster_name, identity:roles, validity
#  ORDER BY validity DESC
# ```

# A dictionary of:
#  cluster names: to a dictionary of:
#     role names: mapping to a tuple of:
#        ( maximum usual validity, expiration datetime for this rule )
CLUSTER_ROLE_MAX_VALIDITIES: Dict[str, Dict[str, Tuple[timedelta, datetime]]] = {
    # "teleport.example.com": {
    #     "example_role": (timedelta(hours=720), datetime(2023, 12, 01, 01, 02, 03)),
    #     "other_example_role": (timedelta(hours=720), datetime.max),
    # },
}


def rule(event):
    if not event.get("event") == "cert.create":
        return False
    max_validity = MAXIMUM_NORMAL_VALIDITY_INTERVAL + ISSUANCE_GRACE_PERIOD
    for role in event.deep_get("identity", "roles", default=[]):
        validity, expiration = CLUSTER_ROLE_MAX_VALIDITIES.get(event.get("cluster_name"), {}).get(
            role, (None, None)
        )
        if validity and expiration:
            # Ignore exceptions that have passed their expiry date
            if datetime.utcnow() < expiration:
                max_validity = max(max_validity, validity)
    return validity_interval(event) > max_validity


def validity_interval(event):
    event_time = panther_nanotime_to_python_datetime(event.get("time"))
    expires = golang_nanotime_to_python_datetime(
        event.deep_get("identity", "expires", default=None)
    )
    if not event_time and expires:
        return False
    interval = expires - event_time
    return interval


def title(event):
    identity = event.deep_get("identity", "user", default="<Cert with no User!?>")
    return (
        f"A Certificate for [{identity}] "
        f"on [{event.get('cluster_name', '<UNKNOWN_CLUSTER>')}] "
        f"has been issued for an unusually long time: {validity_interval(event)!r} "
    )

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.