Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Teleport: Update Rules #966

Merged
merged 12 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions global_helpers/panther_base_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import re
from collections import OrderedDict
from collections.abc import Mapping
from datetime import datetime
from fnmatch import fnmatch
from functools import reduce
from ipaddress import ip_address, ip_network
Expand Down Expand Up @@ -494,3 +495,22 @@ def m365_alert_context(event):
def defang_ioc(ioc):
"""return defanged IOC from 1.1.1.1 to 1[.]1[.]1[.]1"""
return ioc.replace(".", "[.]")


def panther_nanotime_to_python_datetime(panther_time: str) -> datetime:
panther_time_micros = re.search(r"\.(\d+)", panther_time).group(1)
panther_time_micros_rounded = panther_time_micros[0:6]
panther_time_rounded = re.sub(r"\.\d+", f".{panther_time_micros_rounded}", panther_time)
panther_time_format = r"%Y-%m-%d %H:%M:%S.%f"
return datetime.strptime(panther_time_rounded, panther_time_format)


def golang_nanotime_to_python_datetime(golang_time: str) -> datetime:
golang_time_format = r"%Y-%m-%dT%H:%M:%S.%fZ"
# Golang fractional seconds include a mix of microseconds and
# nanoseconds, which doesn't play well with Python's microseconds datetimes.
# This rounds the fractional seconds to a microsecond-size.
golang_time_micros = re.search(r"\.(\d+)Z", golang_time).group(1)
golang_time_micros_rounded = golang_time_micros[0:6]
golang_time_rounded = re.sub(r"\.\d+Z", f".{golang_time_micros_rounded}Z", golang_time)
return datetime.strptime(golang_time_rounded, golang_time_format)
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ def rule(event):
def title(event):
return (
f"A high volume of SSH errors was detected from user "
f"[{event.get('user', '<UNKNOWN_USER>')}]"
f"[{event.get('user', '<UNKNOWN_USER>')}] "
f"on [{event.get('cluster_name', '<UNKNOWN_CLUSTER>')}]"
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Reports:
Description: A high volume of SSH errors could indicate a brute-force attack
Threshold: 10
DedupPeriodMinutes: 15
Reference: https://gravitational.com/teleport/docs/admin-guide/
Reference: https://goteleport.com/docs/management/admin/
Runbook: >
Check that the user making the failed requests legitimately tried logging in that many times.
SummaryAttributes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ def rule(event):


def title(event):
return f"User [{event.get('user', '<UNKNOWN_USER>')}] has manually modified system users"
return (
f"User [{event.get('user', '<UNKNOWN_USER>')}] has manually modified system users "
f"on [{event.get('cluster_name', '<UNKNOWN_CLUSTER>')}]"
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Reports:
Severity: High
Description: A user has been manually created, modified, or deleted
DedupPeriodMinutes: 15
Reference: https://gravitational.com/teleport/docs/admin-guide/
Reference: https://goteleport.com/docs/management/admin/
Runbook: Analyze why it was manually created and delete it if necessary.
SummaryAttributes:
- event
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
SENSITIVE_LOCAL_USERS = ["breakglass"]


def rule(event):
return (
event.get("event") == "user.login"
and event.get("success") == "true"
and event.get("method") == "local"
and not event.get("mfa_device")
)


def severity(event):
if event.get("user") in SENSITIVE_LOCAL_USERS:
return "HIGH"
return "MEDIUM"


def title(event):
return (
f"User [{event.get('user', '<UNKNOWN_USER>')}] logged into "
f"[{event.get('cluster_name', '<UNNAMED_CLUSTER>')}] locally "
f"without using MFA"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
AnalysisType: rule
Filename: teleport_local_user_login_without_mfa.py
RuleID: Teleport.LocalUserLoginWithoutMFA
DisplayName: User Logged in wihout MFA
Enabled: true
LogTypes:
- Gravitational.TeleportAudit
Tags:
- Teleport
Severity: High
Description: A local User logged in without MFA
DedupPeriodMinutes: 60
Reports:
MITRE ATT&CK:
- TA0001:T1078
Reference: https://goteleport.com/docs/management/admin/
Runbook: >
A local user logged in without Multi-Factor Authentication
SummaryAttributes:
- event
- code
- user
- success
- mfa_device
Tests:
-
Name: User logged in with MFA
ExpectedResult: false
Log:
{
"addr.remote": "[2001:db8:feed:face:c0ff:eeb0:baf00:00d]:65123",
"cluster_name": "teleport.example.com",
"code": "T1000I",
"ei": 0,
"event": "user.login",
"method": "local",
"mfa_device": {
"mfa_device_name": "1Password",
"mfa_device_type": "WebAuthn",
"mfa_device_uuid": "88888888-4444-4444-4444-222222222222"
},
"success": true,
"time": "2023-09-20T19:00:00.123456Z",
"uid": "88888888-4444-4444-4444-222222222222",
"user": "max.mustermann",
"user_agent": "Examplecorp Spacedeck-web/99.9 (Hackintosh; ARM Cortex A1000)"
}
-
Name: User logged in without MFA
ExpectedResult: false
Log:
{
"addr.remote": "[2001:db8:face:face:face:face:face:face]:65123",
"cluster_name": "teleport.example.com",
"code": "T1000I",
"ei": 0,
"event": "user.login",
"method": "local",
"success": true,
"time": "2023-09-20T19:00:00.123456Z",
"uid": "88888888-4444-4444-4444-222222222222",
"user": "max.mustermann",
"user_agent": "Examplecorp Spacedeck-web/99.9 (Hackintosh; ARM Cortex A1000)"
}
10 changes: 10 additions & 0 deletions rules/gravitational_teleport_rules/teleport_lock_created.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
def rule(event):
return event.get("event") == "lock.created"


def title(event):
return (
f"A Teleport Lock was created by {event.get('updated_by', '<UNKNOWN_UPDATED_BY>')} "
f"to Lock out user {event.get('target', {}).get('user', '<UNKNOWN_USER>')} "
f"on [{event.get('cluster_name', '<UNKNOWN_CLUSTER>')}]"
)
40 changes: 40 additions & 0 deletions rules/gravitational_teleport_rules/teleport_lock_created.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
AnalysisType: rule
Filename: teleport_lock_created.py
RuleID: Teleport.LockCreated
DisplayName: A Teleport Lock was created
Enabled: true
LogTypes:
- Gravitational.TeleportAudit
Tags:
- Teleport
Severity: Info
Description: A Teleport Lock was created
DedupPeriodMinutes: 60
Reference: https://goteleport.com/docs/management/admin/
Runbook: >
A Teleport Lock was created; this is an unusual administrative action. Investigate to understand why a Lock was created.
SummaryAttributes:
- event
- code
- time
- identity
Tests:
-
Name: A Lock was created
ExpectedResult: true
Log:
{
"cluster_name": "teleport.example.com",
"code": "TLK00I",
"ei": 0,
"event": "lock.created",
"expires": "0001-01-01T00:00:00Z",
"name": "88888888-4444-4444-4444-222222222222",
"target": {
"user": "user-to-disable"
},
"time": "2023-09-21T00:00:00.000000Z",
"uid": "88888888-4444-4444-4444-222222222222",
"updated_by": "[email protected]",
"user": "[email protected]"
}
79 changes: 79 additions & 0 deletions rules/gravitational_teleport_rules/teleport_long_lived_certs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from datetime import timedelta, datetime
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 (
arielkr256 marked this conversation as resolved.
Show resolved Hide resolved
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} "
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ def rule(event):
def title(event):
return (
f"User [{event.get('user', '<UNKNOWN_USER>')}] has issued a network scan with "
f"[{event.get('program', '<UNKNOWN_PROGRAM>')}]"
f"[{event.get('program', '<UNKNOWN_PROGRAM>')}] "
f"on [{event.get('cluster_name', '<UNKNOWN_CLUSTER>')}]"
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ DedupPeriodMinutes: 60
Reports:
MITRE ATT&CK:
- TA0007:T1046
Reference: https://gravitational.com/teleport/docs/admin-guide/
Reference: https://goteleport.com/docs/management/admin/
Runbook: >
Find related commands within the time window and determine if the command was invoked legitimately. Examine the arguments to determine how the command was used.
SummaryAttributes:
Expand Down
Loading