add and create all learn/ folders for intermediate projects
This commit is contained in:
parent
218305e722
commit
e36d2ffdb0
|
|
@ -1,4 +1,4 @@
|
|||
# ⒸAngelaMos | 2025
|
||||
# ⒸAngelaMos | 2026
|
||||
# ---------------------------------------------
|
||||
# API Security Scanner - Environment Variables
|
||||
# Copy this file to .env and update the values
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
ⒸAngelaMos | CarterPerez-dev
|
||||
ⒸCertGames.com | 2025
|
||||
ⒸCertGames.com | 2026
|
||||
----
|
||||
API Security Scanner
|
||||
|
||||
|
|
|
|||
|
|
@ -101,3 +101,4 @@ def get_settings() -> Settings:
|
|||
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
ⒸAngelaMos | 2025
|
||||
ⒸAngelaMos | 2026
|
||||
FastAPI application factory for main.py
|
||||
"""
|
||||
|
||||
|
|
@ -76,3 +76,4 @@ def _register_routes(app: FastAPI) -> None:
|
|||
|
||||
app.include_router(auth_router)
|
||||
app.include_router(scans_router)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
ⒸCertGames.com | 2025
|
||||
ⒸCertGames.com | 2026
|
||||
ⒸAngelaMos | CarterPerez-dev
|
||||
----
|
||||
API Security Scanner FastAPI entry point
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
[project]
|
||||
name = "api-security-scanner-backend"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
description = "Backend API for security testing tool"
|
||||
requires-python = ">=3.11"
|
||||
authors = [ # Replace my Name & Email Here
|
||||
requires-python = ">=3.13"
|
||||
authors = [
|
||||
{name = "CarerPerez-dev", email = "support@certgames.com"}
|
||||
]
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
dependencies = [
|
||||
|
|
@ -17,7 +16,7 @@ dependencies = [
|
|||
# Database
|
||||
"sqlalchemy==2.0.46",
|
||||
"psycopg2-binary==2.9.11",
|
||||
"alembic==1.18.1",
|
||||
"alembic>=1.18.3",
|
||||
# Security
|
||||
"slowapi==0.1.9",
|
||||
"python-jose[cryptography]==3.5.0",
|
||||
|
|
@ -68,7 +67,7 @@ packages = [
|
|||
]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
target-version = "py313"
|
||||
line-length = 95
|
||||
indent-width = 4
|
||||
exclude = [
|
||||
|
|
@ -136,7 +135,7 @@ ignore = [
|
|||
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
python_version = "3.13"
|
||||
mypy_path = "backend"
|
||||
namespace_packages = true
|
||||
explicit_package_bases = true
|
||||
|
|
@ -190,7 +189,7 @@ disable_error_code = [
|
|||
|
||||
|
||||
[tool.pylint.main]
|
||||
py-version = "3.11"
|
||||
py-version = "3.13"
|
||||
jobs = 4
|
||||
load-plugins = [
|
||||
"pylint_pydantic",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# ⒸAngelaMos | 2025
|
||||
# ⒸAngelaMos | 2026
|
||||
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<meta name="description" content="API Security Scanner Project, React Typescript, Nginx, Docker, FastAPI, Python, Pydandic, OWASP">
|
||||
<meta name="author" content="Carter Perez">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="article:author" content="Carterperez-dev - REPLACE MY NAME">
|
||||
<meta property="article:author" content="Carterperez-dev">
|
||||
<meta name="keywords" content="scanner, security, owasp, api scanner, api security, cybersecurity, projects, project">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "api-security-scanner",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,284 +0,0 @@
|
|||
# How The Scanner Is Built
|
||||
|
||||
This doc explains the architecture decisions. Not how to use it, but why it works the way it does.
|
||||
|
||||
## The Layer Cake
|
||||
|
||||
The application follows a standard layered architecture.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Routes (API) │ FastAPI endpoints
|
||||
├─────────────────────────────────────┤
|
||||
│ Services │ Business logic
|
||||
├─────────────────────────────────────┤
|
||||
│ Repositories / Scanners │ Data access / Scanning
|
||||
├─────────────────────────────────────┤
|
||||
│ Models │ SQLAlchemy ORM
|
||||
├─────────────────────────────────────┤
|
||||
│ Database │ PostgreSQL
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Routes** handle HTTP requests, validate input with Pydantic, and return responses. They do not contain business logic.
|
||||
|
||||
**Services** orchestrate operations. ScanService coordinates which scanners to run and saves results. AuthService handles user registration and login.
|
||||
|
||||
**Repositories** abstract database operations. This keeps SQL queries out of services and makes testing easier.
|
||||
|
||||
**Scanners** are the security testing engines. Each scanner inherits from BaseScanner and implements a scan() method.
|
||||
|
||||
## The Scanner Pattern
|
||||
|
||||
All scanners inherit from BaseScanner which provides common functionality.
|
||||
|
||||
```python
|
||||
class BaseScanner(ABC):
|
||||
def __init__(self, target_url, auth_token, max_requests):
|
||||
self.target_url = target_url
|
||||
self.session = self._create_session()
|
||||
# ...
|
||||
|
||||
def make_request(self, method, endpoint, **kwargs):
|
||||
# Rate limiting, retries, timing
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def scan(self) -> TestResultCreate:
|
||||
# Implemented by each scanner
|
||||
pass
|
||||
```
|
||||
|
||||
This design means:
|
||||
- Common HTTP logic lives in one place
|
||||
- Request spacing and retry logic is consistent
|
||||
- Each scanner focuses only on its detection logic
|
||||
- Adding a new scanner is straightforward
|
||||
|
||||
### Request Spacing
|
||||
|
||||
Scanners do not blast requests at targets. Each request is spaced to avoid overwhelming the target or triggering defensive rate limits.
|
||||
|
||||
```python
|
||||
required_delay = 1.0 / (max_requests / window_seconds)
|
||||
```
|
||||
|
||||
With default settings of 100 requests per 60 second window, that is about 600ms between requests. Random jitter is added to avoid predictable patterns.
|
||||
|
||||
This matters because:
|
||||
1. You do not want to DoS your own production systems during testing
|
||||
2. Aggressive scanning triggers alerts and gets you blocked
|
||||
3. Some timing attacks need consistent baseline measurements
|
||||
|
||||
### Retry Logic
|
||||
|
||||
Requests that fail get retried with exponential backoff.
|
||||
|
||||
```python
|
||||
retry_count = 0
|
||||
backoff_factor = 2.0
|
||||
|
||||
while retry_count <= max_retries:
|
||||
response = session.request(method, url)
|
||||
|
||||
if response.status_code == 429:
|
||||
wait_time = int(response.headers.get("Retry-After", default_wait))
|
||||
time.sleep(wait_time)
|
||||
retry_count += 1
|
||||
continue
|
||||
|
||||
if response.status_code >= 500:
|
||||
wait_time = backoff_factor ** retry_count
|
||||
time.sleep(wait_time)
|
||||
retry_count += 1
|
||||
continue
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
429 responses respect the Retry-After header. 5xx errors trigger exponential backoff. This keeps the scanner resilient against temporary failures.
|
||||
|
||||
## Evidence Collection
|
||||
|
||||
Every scan result includes evidence. This is not just for debugging. Proper evidence is required for professional security reports.
|
||||
|
||||
```python
|
||||
evidence = {
|
||||
"status_code": response.status_code,
|
||||
"response_time_ms": elapsed * 1000,
|
||||
"response_length": len(response.text),
|
||||
"headers": self._redact_sensitive_headers(dict(response.headers)),
|
||||
"payload": str(payload),
|
||||
}
|
||||
```
|
||||
|
||||
Sensitive headers are automatically redacted. You do not want authorization tokens showing up in scan reports.
|
||||
|
||||
```python
|
||||
sensitive_headers = [
|
||||
"authorization",
|
||||
"cookie",
|
||||
"x-api-key",
|
||||
"x-auth-token",
|
||||
]
|
||||
```
|
||||
|
||||
## The Service Layer
|
||||
|
||||
ScanService coordinates scans. It maps test types to scanner classes and handles the workflow.
|
||||
|
||||
```python
|
||||
scanner_mapping = {
|
||||
TestType.RATE_LIMIT: RateLimitScanner,
|
||||
TestType.AUTH: AuthScanner,
|
||||
TestType.SQLI: SQLiScanner,
|
||||
TestType.IDOR: IDORScanner,
|
||||
}
|
||||
|
||||
for test_type in scan_request.tests_to_run:
|
||||
scanner_class = scanner_mapping.get(test_type)
|
||||
scanner = scanner_class(target_url, auth_token, max_requests)
|
||||
result = scanner.scan()
|
||||
results.append(result)
|
||||
```
|
||||
|
||||
If a scanner throws an exception, the service catches it and creates an error result. One failing scanner does not kill the entire scan.
|
||||
|
||||
```python
|
||||
except Exception as e:
|
||||
results.append(
|
||||
TestResultCreate(
|
||||
test_name=test_type,
|
||||
status="error",
|
||||
details=f"Scanner error: {str(e)}",
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Database Design
|
||||
|
||||
Three main tables.
|
||||
|
||||
**users**: Account information. Passwords are bcrypt hashed.
|
||||
|
||||
**scans**: Metadata about each scan (who ran it, when, target URL).
|
||||
|
||||
**test_results**: Individual test outcomes linked to scans.
|
||||
|
||||
```
|
||||
users (1) ──── (*) scans (1) ──── (*) test_results
|
||||
```
|
||||
|
||||
Cascade deletes are configured so deleting a user removes their scans, and deleting a scan removes its test results.
|
||||
|
||||
## Rate Limiting The Scanner API
|
||||
|
||||
The scanner API itself is rate limited using slowapi.
|
||||
|
||||
```python
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
@router.post("/")
|
||||
@limiter.limit("5/minute")
|
||||
async def create_scan(...):
|
||||
pass
|
||||
```
|
||||
|
||||
Running security scans is expensive. Without rate limiting, someone could hammer your scanner with requests and either run up your bills or use it to attack third parties.
|
||||
|
||||
The scan endpoint has stricter limits than read endpoints. Creating a scan triggers potentially hundreds of requests to the target.
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
JWT tokens with bcrypt password hashing.
|
||||
|
||||
```
|
||||
1. User registers with email/password
|
||||
2. Password is hashed with bcrypt (cost factor 12)
|
||||
3. User logs in with credentials
|
||||
4. Server validates password against hash
|
||||
5. Server issues JWT with user ID and expiration
|
||||
6. Client sends JWT in Authorization header
|
||||
7. Server validates JWT on each request
|
||||
```
|
||||
|
||||
The JWT contains minimal claims. Just enough to identify the user.
|
||||
|
||||
```python
|
||||
{
|
||||
"sub": "user_id",
|
||||
"exp": expiration_timestamp
|
||||
}
|
||||
```
|
||||
|
||||
No roles or permissions in the token. Those are checked against the database on each request. This means you can revoke permissions instantly without waiting for token expiration.
|
||||
|
||||
## Error Handling Strategy
|
||||
|
||||
The application uses HTTPException for expected errors and lets unexpected errors bubble up.
|
||||
|
||||
```python
|
||||
if not scan:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Scan not found",
|
||||
)
|
||||
|
||||
if scan.user_id != user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to access this scan",
|
||||
)
|
||||
```
|
||||
|
||||
FastAPI catches HTTPException and returns proper JSON error responses. Unhandled exceptions return 500 with minimal details (no stack traces in production).
|
||||
|
||||
## Configuration
|
||||
|
||||
Settings are loaded from environment variables with pydantic_settings.
|
||||
|
||||
```python
|
||||
class Settings(BaseSettings):
|
||||
BACKEND_HOST: str = "0.0.0.0"
|
||||
BACKEND_PORT: int = 8000
|
||||
DEBUG: bool = False
|
||||
DATABASE_URL: str
|
||||
JWT_SECRET_KEY: str
|
||||
# ...
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
```
|
||||
|
||||
Defaults exist for development convenience but secrets like JWT_SECRET_KEY have no default. The application fails fast if required settings are missing.
|
||||
|
||||
## Why This Structure
|
||||
|
||||
The layered approach might seem like overkill for a scanner, but it pays off.
|
||||
|
||||
**Testing**: Each layer can be tested independently. Mock the repository to test services. Mock services to test routes.
|
||||
|
||||
**Extensibility**: Adding a new scanner means creating one file that inherits from BaseScanner. No changes to services or routes needed.
|
||||
|
||||
**Maintainability**: Database logic stays in repositories. HTTP logic stays in routes. Business logic stays in services. Changes are localized.
|
||||
|
||||
**Security**: Separation of concerns makes security review easier. Auth checks happen in one place. Input validation happens in one place.
|
||||
|
||||
## Adding A New Scanner
|
||||
|
||||
To add a new vulnerability scanner:
|
||||
|
||||
1. Create a new file in `scanners/`
|
||||
2. Inherit from BaseScanner
|
||||
3. Implement the scan() method
|
||||
4. Add payloads to payloads.py if needed
|
||||
5. Add the test type to core/enums.py
|
||||
6. Add mapping in ScanService
|
||||
|
||||
```python
|
||||
class XSSScanner(BaseScanner):
|
||||
def scan(self) -> TestResultCreate:
|
||||
# Detection logic here
|
||||
pass
|
||||
```
|
||||
|
||||
The scanner pattern handles HTTP requests, retries, and evidence collection. You focus on the detection logic.
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
# How Security Scanning Actually Works
|
||||
|
||||
This doc covers the techniques behind API security scanning. How detection works, what makes a good payload, and the tradeoffs between thoroughness and noise.
|
||||
|
||||
## The Scanning Process
|
||||
|
||||
Every scanner follows the same general flow.
|
||||
|
||||
```
|
||||
1. Establish baseline (normal behavior)
|
||||
2. Send test payloads
|
||||
3. Observe differences from baseline
|
||||
4. Classify findings
|
||||
5. Collect evidence
|
||||
```
|
||||
|
||||
The key insight: vulnerability detection is about finding anomalies. Normal requests produce normal responses. Malicious requests produce different responses. The difference reveals the vulnerability.
|
||||
|
||||
## Baseline Timing
|
||||
|
||||
Time based attacks need accurate baselines. Network latency varies. Server response time varies. You need to distinguish between normal variance and injected delays.
|
||||
|
||||
```python
|
||||
def get_baseline_timing(endpoint, samples=5):
|
||||
times = []
|
||||
for _ in range(samples):
|
||||
response = make_request("GET", endpoint)
|
||||
times.append(response.elapsed)
|
||||
time.sleep(0.5)
|
||||
|
||||
return mean(times), stdev(times)
|
||||
```
|
||||
|
||||
Taking multiple samples and calculating standard deviation lets you set a proper threshold.
|
||||
|
||||
```python
|
||||
threshold = baseline_mean + (3 * baseline_stdev)
|
||||
```
|
||||
|
||||
If your baseline is 200ms with a standard deviation of 50ms, your threshold is 350ms. A response taking 5.2 seconds when you injected a 5 second delay is clearly the attack working, not network issues.
|
||||
|
||||
## Payload Design
|
||||
|
||||
Good payloads are specific. They test one thing and produce a clear signal.
|
||||
|
||||
### SQL Injection Payloads
|
||||
|
||||
Basic auth bypass payloads:
|
||||
```
|
||||
' OR '1'='1
|
||||
' OR 1=1--
|
||||
admin'--
|
||||
```
|
||||
|
||||
These work against string fields in WHERE clauses. The quote breaks out of the value, the OR makes the condition always true, and the comment (-- or #) ignores the rest of the query.
|
||||
|
||||
Time delay payloads vary by database:
|
||||
```
|
||||
'; WAITFOR DELAY '0:0:5'-- (MSSQL)
|
||||
'; SELECT SLEEP(5)-- (MySQL)
|
||||
'; pg_sleep(5)-- (PostgreSQL)
|
||||
```
|
||||
|
||||
Each database has different syntax for delays. The scanner tries multiple variants to identify which database is in use.
|
||||
|
||||
Union payloads for data extraction:
|
||||
```
|
||||
' UNION SELECT NULL--
|
||||
' UNION SELECT NULL,NULL--
|
||||
' UNION SELECT username,password FROM users--
|
||||
```
|
||||
|
||||
The number of NULLs must match the column count of the original query. Scanners typically try 1, 2, 3, etc. until one works.
|
||||
|
||||
### Authentication Payloads
|
||||
|
||||
JWT none algorithm variants:
|
||||
```python
|
||||
["none", "None", "NONE", "nOnE", "NoNe"]
|
||||
```
|
||||
|
||||
Case variations catch libraries that only check for lowercase "none" but accept mixed case.
|
||||
|
||||
Invalid tokens to test error handling:
|
||||
```python
|
||||
["", "invalid", "null", "undefined", "' OR '1'='1"]
|
||||
```
|
||||
|
||||
If any of these get a 200 response, the token validation is broken.
|
||||
|
||||
### Rate Limit Bypass Headers
|
||||
|
||||
```python
|
||||
[
|
||||
{"X-Forwarded-For": "127.0.0.1"},
|
||||
{"X-Real-IP": "10.0.0.1"},
|
||||
{"X-Originating-IP": "192.168.1.1"},
|
||||
{"X-Client-IP": "8.8.8.8"},
|
||||
{"CF-Connecting-IP": "1.1.1.1"},
|
||||
{"True-Client-IP": "172.16.0.1"},
|
||||
]
|
||||
```
|
||||
|
||||
Each header is tested with rotating IP values. If requests with different spoofed IPs all succeed while requests without the header get blocked, the rate limiter trusts that header.
|
||||
|
||||
## Response Analysis
|
||||
|
||||
Different vulnerabilities produce different signals.
|
||||
|
||||
### Error Messages
|
||||
|
||||
SQL injection often produces database errors.
|
||||
|
||||
```python
|
||||
error_signatures = {
|
||||
"mysql": ["sql syntax", "mysql_fetch", "warning: mysql"],
|
||||
"postgres": ["pg_query", "pg_exec", "pgsql"],
|
||||
"mssql": ["odbc sql server", "sqlexception"],
|
||||
"oracle": ["ora-", "pl/sql"],
|
||||
}
|
||||
|
||||
for db_type, signatures in error_signatures.items():
|
||||
for signature in signatures:
|
||||
if signature in response.text.lower():
|
||||
return {"vulnerable": True, "database_type": db_type}
|
||||
```
|
||||
|
||||
The error message also reveals the database type, which informs which payloads to try next.
|
||||
|
||||
### Response Length Differences
|
||||
|
||||
Boolean based blind SQLi detection relies on response length.
|
||||
|
||||
```python
|
||||
true_response = make_request("GET", "/?id=1 AND 1=1")
|
||||
false_response = make_request("GET", "/?id=1 AND 1=2")
|
||||
|
||||
length_diff = abs(len(true_response.text) - len(false_response.text))
|
||||
|
||||
if length_diff > 100:
|
||||
return {"vulnerable": True, "confidence": "HIGH" if length_diff > 500 else "MEDIUM"}
|
||||
```
|
||||
|
||||
A large difference means the database is evaluating the boolean condition. The query structure is being modified by the input.
|
||||
|
||||
### Status Codes
|
||||
|
||||
Status codes reveal a lot.
|
||||
|
||||
| Code | Meaning for Testing |
|
||||
|------|---------------------|
|
||||
| 200 | Request succeeded, possibly vulnerable |
|
||||
| 401 | Authentication required (expected for auth tests) |
|
||||
| 403 | Forbidden (authorization working) |
|
||||
| 404 | Resource not found (might be good for IDOR) |
|
||||
| 429 | Rate limited (rate limiter working) |
|
||||
| 500 | Server error (might indicate crash from payload) |
|
||||
|
||||
A 200 on an endpoint that should require auth is a vulnerability. A 429 when testing rate limits means they work. A 500 from a SQL payload might indicate injection but also might just be a crash.
|
||||
|
||||
### Timing
|
||||
|
||||
Timing analysis requires statistical rigor.
|
||||
|
||||
```python
|
||||
baseline_mean, baseline_stdev = get_baseline_timing("/")
|
||||
delay_seconds = 5
|
||||
expected_delay = baseline_mean + delay_seconds
|
||||
|
||||
for payload in time_based_payloads:
|
||||
times = []
|
||||
for _ in range(3): # Multiple samples per payload
|
||||
response = make_request("GET", f"/?id={payload}")
|
||||
times.append(response.elapsed)
|
||||
time.sleep(1)
|
||||
|
||||
avg_time = mean(times)
|
||||
|
||||
if avg_time >= expected_delay - 1: # Allow 1 second tolerance
|
||||
return {"vulnerable": True}
|
||||
```
|
||||
|
||||
Three samples per payload reduces false positives from network hiccups. The tolerance accounts for timing imprecision.
|
||||
|
||||
## False Positives and Confidence
|
||||
|
||||
Not every anomaly is a vulnerability. Scanners need to distinguish real findings from noise.
|
||||
|
||||
### High Confidence Indicators
|
||||
|
||||
- Database error strings in response (SQLi)
|
||||
- Token with "none" algorithm accepted (JWT)
|
||||
- 5+ second response when 5 second delay injected (Time based SQLi)
|
||||
- Different IDs accessible with same auth token (IDOR)
|
||||
|
||||
### Lower Confidence Indicators
|
||||
|
||||
- Response length differences (might be caching)
|
||||
- Single timing anomaly (network variance)
|
||||
- 500 errors (might be unrelated crash)
|
||||
|
||||
The scanner assigns confidence levels based on how definitive the evidence is.
|
||||
|
||||
```python
|
||||
if response_time >= expected_delay:
|
||||
confidence = "HIGH"
|
||||
elif response_time >= expected_delay * 0.8:
|
||||
confidence = "MEDIUM"
|
||||
```
|
||||
|
||||
## Scanning Safely
|
||||
|
||||
Security scanning can break things. These practices minimize damage.
|
||||
|
||||
### Request Spacing
|
||||
|
||||
Never send requests as fast as possible.
|
||||
|
||||
```python
|
||||
required_delay = 1.0 / (max_requests / window_seconds)
|
||||
time.sleep(required_delay + random_jitter)
|
||||
```
|
||||
|
||||
This protects:
|
||||
- Target servers from being overwhelmed
|
||||
- Your scanner from being blocked
|
||||
- Timing measurements from being skewed
|
||||
|
||||
### Read Only Operations
|
||||
|
||||
The scanner uses GET requests where possible and avoids destructive payloads.
|
||||
|
||||
```python
|
||||
# Testing payloads
|
||||
"' OR '1'='1" # Yes: tests injection without modification
|
||||
"'; DROP TABLE--" # Included but dangerous on real data
|
||||
```
|
||||
|
||||
Even though the payloads include statements like DROP TABLE, the scanner is testing for the vulnerability, not exploiting it. If the database is vulnerable, the SELECT from the OR clause would work anyway.
|
||||
|
||||
### Authentication Scope
|
||||
|
||||
When testing authenticated endpoints, the scanner uses the provided token consistently.
|
||||
|
||||
```python
|
||||
if self.auth_token:
|
||||
session.headers.update({"Authorization": f"Bearer {self.auth_token}"})
|
||||
```
|
||||
|
||||
This means:
|
||||
- Tests stay within authorized scope
|
||||
- IDOR tests check if you can access other users' data, not if you can bypass auth entirely
|
||||
- Results are relevant to your access level
|
||||
|
||||
### Error Recovery
|
||||
|
||||
Failed requests do not crash the scan.
|
||||
|
||||
```python
|
||||
try:
|
||||
result = scanner.scan()
|
||||
except Exception as e:
|
||||
result = TestResultCreate(
|
||||
status="error",
|
||||
details=f"Scanner error: {str(e)}",
|
||||
)
|
||||
```
|
||||
|
||||
Network timeouts, connection resets, and unexpected responses are handled. The scan continues with remaining tests.
|
||||
|
||||
## Interpreting Results
|
||||
|
||||
Scanner output needs human interpretation.
|
||||
|
||||
### Vulnerable Status
|
||||
|
||||
The scanner found evidence of a vulnerability. Review the evidence to confirm:
|
||||
- Is the payload visible in the evidence?
|
||||
- Does the detection method match the vulnerability type?
|
||||
- Could this be a false positive?
|
||||
|
||||
### Safe Status
|
||||
|
||||
No vulnerability detected with the payloads tested. This does not mean the target is secure. It means these specific tests passed.
|
||||
|
||||
```
|
||||
"No SQL injection vulnerabilities detected"
|
||||
|
||||
What this means: Standard SQLi payloads did not trigger errors or timing anomalies
|
||||
What this does NOT mean: The application is immune to SQL injection
|
||||
```
|
||||
|
||||
Different payloads, different endpoints, or different parameters might reveal vulnerabilities the scan missed.
|
||||
|
||||
### Error Status
|
||||
|
||||
The test could not complete. Check the error message:
|
||||
- Connection refused: Target is down or blocking your IP
|
||||
- Timeout: Target is slow or has aggressive rate limiting
|
||||
- SSL error: Certificate issues
|
||||
|
||||
Errors mean you need to investigate before trusting the result.
|
||||
|
||||
## Limitations
|
||||
|
||||
No scanner finds everything. Understanding limitations helps you use results appropriately.
|
||||
|
||||
**Coverage**: The scanner tests a predefined set of payloads and techniques. Novel attack vectors are not covered.
|
||||
|
||||
**Depth**: Automated scanning is shallow compared to manual testing. Complex logic flaws require human analysis.
|
||||
|
||||
**Context**: The scanner does not understand your business logic. It cannot tell if a vulnerability matters for your specific application.
|
||||
|
||||
**Evasion**: Sophisticated applications might detect and block scanning patterns. Low and slow scanning helps but is not foolproof.
|
||||
|
||||
**False Negatives**: Passing all tests does not mean the application is secure. It means these specific tests passed against these specific endpoints with these specific inputs.
|
||||
|
||||
Use automated scanning as one layer of security testing, not the only layer. Combine with code review, manual penetration testing, and ongoing monitoring.
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
# API Vulnerabilities This Scanner Actually Detects
|
||||
|
||||
This scanner tests for four categories of vulnerabilities. Each one maps to real OWASP API Security Top 10 entries. Here is what they are, how attackers exploit them, and why they matter.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Vulnerability | OWASP ID | Severity | What Happens |
|
||||
|---------------|----------|----------|--------------|
|
||||
| IDOR/BOLA | API1:2023 | High | Attacker accesses other users data |
|
||||
| Broken Auth | API2:2023 | Critical | Attacker bypasses login entirely |
|
||||
| SQL Injection | API8:2023 | Critical | Attacker reads/modifies your database |
|
||||
| Missing Rate Limits | API4:2023 | High | Attacker brute forces or DoS attacks |
|
||||
|
||||
## IDOR/BOLA (Broken Object Level Authorization)
|
||||
|
||||
OWASP ranks this number one for a reason. It is the most common API vulnerability in the wild.
|
||||
|
||||
The problem: your API checks if a user is logged in, but not if they own the resource they are requesting.
|
||||
|
||||
```
|
||||
GET /api/users/42/orders
|
||||
|
||||
User 42: sees their orders (correct)
|
||||
User 99: also sees user 42's orders (vulnerable)
|
||||
```
|
||||
|
||||
The scanner tests this by:
|
||||
1. Extracting IDs from API responses (both numeric and UUIDs)
|
||||
2. Trying to access resources with manipulated IDs
|
||||
3. Checking if sequential IDs are predictable
|
||||
|
||||
Real world example: In 2019, First American Financial exposed 885 million records because their document URLs used sequential IDs with no authorization check. Change the number in the URL, see someone else's mortgage documents.
|
||||
|
||||
### Why UUIDs Do Not Fix This
|
||||
|
||||
Some developers think switching from numeric IDs to UUIDs solves IDOR. It does not.
|
||||
|
||||
UUIDs make enumeration harder, not impossible. If an attacker finds one UUID (from a shared link, email, logs, or browser history), they can still access that resource without authorization.
|
||||
|
||||
```
|
||||
GET /api/documents/550e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
The fix is always authorization checks. Verify the requesting user has permission to access the specific resource, regardless of ID format.
|
||||
|
||||
## Broken Authentication (API2:2023)
|
||||
|
||||
Authentication vulnerabilities let attackers log in as other users or bypass authentication entirely.
|
||||
|
||||
### Missing Authentication
|
||||
|
||||
The most obvious case: endpoints that should require login but do not.
|
||||
|
||||
```python
|
||||
# Vulnerable: no auth required
|
||||
@app.get("/api/admin/users")
|
||||
def get_all_users():
|
||||
return db.query(User).all()
|
||||
```
|
||||
|
||||
The scanner tests this by making requests without any authentication headers and checking if it gets a 200 response instead of 401.
|
||||
|
||||
### JWT Vulnerabilities
|
||||
|
||||
JWTs have specific implementation bugs that keep appearing.
|
||||
|
||||
**The None Algorithm Attack**
|
||||
|
||||
JWTs have three parts: header, payload, signature. The header specifies which algorithm verifies the signature. Some libraries accept "none" as a valid algorithm, meaning no signature required.
|
||||
|
||||
```
|
||||
Original header: {"alg": "HS256", "typ": "JWT"}
|
||||
Malicious header: {"alg": "none", "typ": "JWT"}
|
||||
```
|
||||
|
||||
If the server accepts this, an attacker can forge any token they want. The scanner tests multiple case variations because some libraries only check for lowercase "none".
|
||||
|
||||
```python
|
||||
# These all bypass poorly implemented JWT validation
|
||||
"none", "None", "NONE", "nOnE"
|
||||
```
|
||||
|
||||
**Signature Removal**
|
||||
|
||||
Related to the none algorithm: some implementations check if the algorithm is "none" but still accept tokens with empty signatures.
|
||||
|
||||
```
|
||||
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.
|
||||
^ empty signature
|
||||
```
|
||||
|
||||
The scanner removes signatures from valid tokens and tests if the server still accepts them.
|
||||
|
||||
### Invalid Token Handling
|
||||
|
||||
Good APIs reject malformed tokens. Bad APIs might accept them or crash.
|
||||
|
||||
The scanner sends garbage tokens and checks for 200 responses:
|
||||
- Empty strings
|
||||
- Random characters
|
||||
- SQL injection payloads in tokens
|
||||
- Path traversal strings
|
||||
|
||||
If any of these return 200, something is wrong with your token validation.
|
||||
|
||||
## SQL Injection (API8:2023)
|
||||
|
||||
SQL injection has been around for 25 years and still makes the top 10 because developers keep building queries with string concatenation.
|
||||
|
||||
### Error Based Detection
|
||||
|
||||
The easiest SQLi to find. Send a malformed query, get a database error in the response.
|
||||
|
||||
```
|
||||
GET /api/users?id=' OR '1'='1
|
||||
|
||||
Response: "You have an error in your SQL syntax near..."
|
||||
```
|
||||
|
||||
The scanner looks for error signatures from MySQL, PostgreSQL, MSSQL, and Oracle. Each database has distinctive error messages.
|
||||
|
||||
| Database | Error Contains |
|
||||
|----------|----------------|
|
||||
| MySQL | "mysql_fetch", "sql syntax", "warning: mysql" |
|
||||
| PostgreSQL | "pg_query", "pg_exec", "pgsql" |
|
||||
| MSSQL | "odbc sql server", "sqlexception" |
|
||||
| Oracle | "ora-", "pl/sql" |
|
||||
|
||||
Finding these errors means two things: the input reaches the database unsanitized, and error messages are exposed to users. Both are bad.
|
||||
|
||||
### Boolean Based Blind
|
||||
|
||||
When errors are hidden, you can still detect SQLi by comparing responses.
|
||||
|
||||
```
|
||||
GET /api/users?id=1 AND 1=1 (true condition)
|
||||
GET /api/users?id=1 AND 1=2 (false condition)
|
||||
```
|
||||
|
||||
If the true condition returns normal results and the false condition returns empty or different results, the input is being evaluated as SQL.
|
||||
|
||||
The scanner measures response length differences. A significant difference (over 100 bytes typically) between true and false conditions indicates a vulnerability.
|
||||
|
||||
### Time Based Blind
|
||||
|
||||
The hardest to detect but most reliable for confirming SQLi.
|
||||
|
||||
```
|
||||
GET /api/users?id=1; WAITFOR DELAY '0:0:5'-- (MSSQL)
|
||||
GET /api/users?id=1'; SELECT SLEEP(5)-- (MySQL)
|
||||
GET /api/users?id=1'; pg_sleep(5)-- (PostgreSQL)
|
||||
```
|
||||
|
||||
If the response takes 5 seconds longer than normal, the database executed the delay. This works even when the application shows no visible difference in output.
|
||||
|
||||
The scanner:
|
||||
1. Establishes baseline response times with multiple samples
|
||||
2. Calculates standard deviation to account for network variance
|
||||
3. Sends delay payloads and measures actual response time
|
||||
4. Compares against expected delay time (baseline + injected delay)
|
||||
|
||||
False positives are rare with time based testing because network latency does not consistently add exactly 5 seconds.
|
||||
|
||||
## Rate Limiting (API4:2023)
|
||||
|
||||
APIs without rate limiting are vulnerable to brute force attacks, credential stuffing, and denial of service.
|
||||
|
||||
### Detection
|
||||
|
||||
The scanner makes multiple requests and looks for:
|
||||
1. Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, etc.)
|
||||
2. 429 Too Many Requests responses
|
||||
3. Retry-After headers
|
||||
|
||||
Finding rate limit headers but never hitting 429 means the limits exist in name only. The scanner flags this as "headers only" which is a medium severity issue.
|
||||
|
||||
### Bypass Testing
|
||||
|
||||
Rate limiters often have implementation bugs.
|
||||
|
||||
**IP Header Spoofing**
|
||||
|
||||
Many rate limiters use client IP for tracking. If they trust X-Forwarded-For or similar headers, attackers can bypass limits by rotating fake IPs.
|
||||
|
||||
```
|
||||
X-Forwarded-For: 10.0.0.1
|
||||
X-Forwarded-For: 10.0.0.2
|
||||
X-Forwarded-For: 10.0.0.3
|
||||
```
|
||||
|
||||
The scanner tests X-Forwarded-For, X-Real-IP, X-Client-IP, X-Originating-IP, CF-Connecting-IP, and True-Client-IP. If any of these bypass rate limits, attackers can make unlimited requests.
|
||||
|
||||
**Endpoint Variations**
|
||||
|
||||
Some rate limiters are case sensitive or miss URL variations.
|
||||
|
||||
```
|
||||
/api/login (rate limited)
|
||||
/API/LOGIN (not rate limited?)
|
||||
/api/login/ (not rate limited?)
|
||||
/api/./login (not rate limited?)
|
||||
```
|
||||
|
||||
The scanner tests path variations including case changes, trailing slashes, and encoded characters.
|
||||
|
||||
## What The Severity Ratings Mean
|
||||
|
||||
The scanner uses five severity levels.
|
||||
|
||||
**Critical**: Immediate risk of data breach or system compromise. SQL injection and JWT bypass fall here. Fix before anything else.
|
||||
|
||||
**High**: Serious risk that needs prompt attention. IDOR and missing rate limits on auth endpoints. Attackers will find and exploit these.
|
||||
|
||||
**Medium**: Moderate risk. Rate limit headers without enforcement, or predictable ID patterns. Still needs fixing, but less urgent.
|
||||
|
||||
**Low**: Minor issues. Might indicate poor practices but no immediate exploit path.
|
||||
|
||||
**Info**: Informational findings. The test passed or found something worth noting.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- OWASP API Security Top 10 2023: https://owasp.org/API-Security/
|
||||
- PortSwigger Web Security Academy: https://portswigger.net/web-security
|
||||
- HackTricks API Pentesting: https://book.hacktricks.wiki/en/network-services-pentesting/pentesting-web/api-pentesting.html
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"name": "api-security-scanner",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Automated API security testing tool with FastAPI backend and ReactTS frontend",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "docker-compose -f docker-compose.dev.yml up",
|
||||
"dev": "docker compose -f docker-compose.dev.yml up",
|
||||
"dev:build": "docker compose -f docker-compose.dev.yml up --build",
|
||||
"dev:down": "docker compose -f docker-compose.dev.yml down",
|
||||
"dev:logs": "docker compose -f docker-compose.dev.yml logs -f",
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/carterperez-dev/api-security-scanner"
|
||||
"url": "https://github.com/CarterPerez-dev/Cybersecurity-Projects/PROJECTS/intermediate/api-security-scanner"
|
||||
},
|
||||
"keywords": [
|
||||
"security",
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
"react",
|
||||
"docker"
|
||||
],
|
||||
"author": "CarerPerez-dev - also replace my github url username with yours",
|
||||
"author": "https://github.com/CarterPerez-dev",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,186 @@ cmd/docksec/ Entry point and CLI commands
|
|||
internal/
|
||||
analyzer/ Security checks for different target types
|
||||
benchmark/ CIS Docker Benchmark control definitions
|
||||
config/ Runtime configuration and constants
|
||||
config/ Runtime configuration and constants# Docker Security Audit Tool (docksec)
|
||||
|
||||
## What This Is
|
||||
|
||||
A Go-based CLI tool that scans Docker environments for security misconfigurations and validates them against the CIS Docker Benchmark. It analyzes running containers, daemon settings, images, Dockerfiles, and docker-compose files to identify vulnerabilities like privileged containers, dangerous capabilities, Docker socket mounts, and hardcoded secrets.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Docker containers don't provide security by default. A single misconfiguration can give an attacker complete control over your host system. This tool catches those mistakes before they become breaches.
|
||||
|
||||
**Real world scenarios where this applies:**
|
||||
|
||||
- **Container escape via Docker socket mount**: In 2018, Tesla's Kubernetes cluster was compromised when attackers found a pod with `/var/run/docker.sock` mounted, letting them escape to the host and mine cryptocurrency.
|
||||
|
||||
- **Privilege escalation through capabilities**: The 2014 "Shocker" exploit used `CAP_DAC_READ_SEARCH` to read arbitrary files from the host filesystem, bypassing container isolation completely.
|
||||
|
||||
- **Secret exposure in images**: In 2019, over 100,000 Docker images on Docker Hub contained embedded API keys, passwords, and private keys discoverable through simple text searches.
|
||||
|
||||
## What You'll Learn
|
||||
|
||||
This project teaches you how container security actually works under the hood. By building and extending it, you'll understand:
|
||||
|
||||
**Security Concepts:**
|
||||
- **CIS Docker Benchmark compliance** - industry standard security controls covering host config, daemon settings, images, and runtime. You'll learn which of the 100+ controls matter most and why.
|
||||
- **Linux capabilities model** - how the 41 discrete privileges work, which ones enable container escape (like `CAP_SYS_ADMIN` and `CAP_SYS_PTRACE`), and how to audit them programmatically.
|
||||
- **Namespace isolation boundaries** - what PID, network, IPC, and mount namespaces protect against, and how sharing host namespaces breaks that protection.
|
||||
- **Security profiles (seccomp, AppArmor, SELinux)** - how syscall filtering and mandatory access control actually prevent attacks, not just theoretical defense-in-depth concepts.
|
||||
- **Secret detection techniques** - pattern matching with regex, entropy analysis using Shannon entropy, and why both are needed to catch different types of leaked credentials.
|
||||
|
||||
**Technical Skills:**
|
||||
- **Docker API interaction** - using the official Docker client library to introspect containers, images, and daemon configuration without shell commands.
|
||||
- **Static analysis of build files** - parsing Dockerfiles with AST (abstract syntax tree) to detect security issues at build time, before images ever run.
|
||||
- **Concurrent scanning** - using Go's errgroup and rate limiters to scan hundreds of containers efficiently without overwhelming the daemon.
|
||||
- **Security reporting formats** - generating SARIF (Static Analysis Results Interchange Format) for GitHub Security, JUnit for CI/CD, and structured JSON for automation.
|
||||
|
||||
**Tools and Techniques:**
|
||||
- **moby/buildkit parser** - the same parser Docker uses to understand Dockerfile syntax, giving you access to instruction metadata and line numbers for precise findings.
|
||||
- **CIS Benchmark mapping** - translating security controls into automated checks, including determining which findings are "scored" vs "not scored" in compliance reporting.
|
||||
- **YAML node traversal** - navigating docker-compose files as structured data rather than text, handling both mapping and sequence formats for the same logical configuration.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, you should understand:
|
||||
|
||||
**Required knowledge:**
|
||||
- **Go basics** - structs, interfaces, goroutines, channels. You'll be reading and modifying Go code that uses errgroups for concurrency and rate limiters for API throttling.
|
||||
- **Docker fundamentals** - images vs containers, how volumes work, what ports do. You need to know the difference between `docker run --privileged` and `--cap-add=NET_ADMIN`.
|
||||
- **Linux security model** - what root can do, basic permission concepts, why running as UID 0 is dangerous. Understanding `/proc` and `/sys` helps but isn't required.
|
||||
- **Command line proficiency** - comfortable with `docker inspect`, reading JSON output, understanding exit codes and stderr vs stdout.
|
||||
|
||||
**Tools you'll need:**
|
||||
- **Go 1.23+** - the project uses generics and newer error handling patterns
|
||||
- **Docker Engine** - running locally or accessible via `DOCKER_HOST`. Docker Desktop on Mac/Windows works fine.
|
||||
- **Make** (optional) - for running build and test commands conveniently
|
||||
- **Git** - to clone the repository and track your changes
|
||||
|
||||
**Helpful but not required:**
|
||||
- Experience with security scanning tools like Trivy, Snyk, or Anchore
|
||||
- Familiarity with the CIS Benchmark PDFs (the tool teaches you the controls)
|
||||
- Knowledge of Go testing and benchmarking
|
||||
|
||||
## Quick Start
|
||||
|
||||
Get the project running locally:
|
||||
|
||||
```bash
|
||||
# Clone and navigate
|
||||
cd PROJECTS/intermediate/docker-security-audit
|
||||
|
||||
# Build the binary
|
||||
go build -o docksec ./cmd/docksec
|
||||
|
||||
# Scan all running containers
|
||||
./docksec scan
|
||||
|
||||
# Scan specific targets
|
||||
./docksec scan --target containers
|
||||
./docksec scan --target daemon
|
||||
./docksec scan --target images
|
||||
|
||||
# Scan a Dockerfile
|
||||
./docksec scan --file Dockerfile
|
||||
|
||||
# Scan a docker-compose file
|
||||
./docksec scan --file docker-compose.yml
|
||||
|
||||
# Generate JSON output for automation
|
||||
./docksec scan --output json --output-file results.json
|
||||
|
||||
# Generate SARIF for GitHub Security
|
||||
./docksec scan --output sarif --output-file results.sarif
|
||||
|
||||
# Filter by severity
|
||||
./docksec scan --severity high,critical
|
||||
|
||||
# Fail CI builds on findings
|
||||
./docksec scan --fail-on medium
|
||||
```
|
||||
|
||||
Expected output: Terminal report grouped by category (Container Runtime, Docker Daemon, etc.) showing findings with severity levels, CIS control IDs, descriptions, and remediation steps. Zero findings if your Docker environment is properly secured.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
docker-security-audit/
|
||||
├── cmd/
|
||||
│ └── docksec/
|
||||
│ └── main.go # CLI entry point, Cobra commands
|
||||
├── internal/
|
||||
│ ├── analyzer/
|
||||
│ │ ├── analyzer.go # Analyzer interface
|
||||
│ │ ├── container.go # Running container checks
|
||||
│ │ ├── daemon.go # Docker daemon config checks
|
||||
│ │ ├── image.go # Image metadata checks
|
||||
│ │ ├── dockerfile.go # Dockerfile static analysis
|
||||
│ │ └── compose.go # docker-compose.yml checks
|
||||
│ ├── benchmark/
|
||||
│ │ └── controls.go # CIS Docker Benchmark control registry
|
||||
│ ├── config/
|
||||
│ │ ├── config.go # Configuration struct and filters
|
||||
│ │ └── constants.go # Rate limits, timeouts, thresholds
|
||||
│ ├── docker/
|
||||
│ │ └── client.go # Docker API client wrapper
|
||||
│ ├── finding/
|
||||
│ │ └── finding.go # Finding data model and collection
|
||||
│ ├── proc/
|
||||
│ │ ├── capabilities.go # Linux capabilities parsing
|
||||
│ │ ├── proc.go # /proc filesystem reading
|
||||
│ │ └── security.go # Security profile inspection
|
||||
│ ├── report/
|
||||
│ │ ├── reporter.go # Reporter interface and factory
|
||||
│ │ ├── terminal.go # Human-readable output
|
||||
│ │ ├── json.go # Structured JSON output
|
||||
│ │ ├── sarif.go # SARIF 2.1.0 format
|
||||
│ │ └── junit.go # JUnit XML for CI/CD
|
||||
│ ├── rules/
|
||||
│ │ ├── capabilities.go # 41 Linux capabilities with risk levels
|
||||
│ │ ├── paths.go # 200+ sensitive host paths
|
||||
│ │ └── secrets.go # 80+ secret patterns and entropy
|
||||
│ └── scanner/
|
||||
│ └── scanner.go # Main scan orchestration
|
||||
├── Dockerfile # Multi-stage build for container deployment
|
||||
├── go.mod # Dependencies
|
||||
└── go.sum
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Understand the concepts** - Read [01-CONCEPTS.md](./01-CONCEPTS.md) to learn how Linux capabilities, namespaces, and security profiles actually work at the kernel level.
|
||||
|
||||
2. **Study the architecture** - Read [02-ARCHITECTURE.md](./02-ARCHITECTURE.md) to see how the scanner orchestrates concurrent analyzers, applies rules, and generates findings.
|
||||
|
||||
3. **Walk through the code** - Read [03-IMPLEMENTATION.md](./03-IMPLEMENTATION.md) for detailed implementation walkthroughs with actual code from the project.
|
||||
|
||||
4. **Extend the project** - Read [04-CHALLENGES.md](./04-CHALLENGES.md) for specific ideas to add features like runtime monitoring, Kubernetes integration, and custom policies.
|
||||
|
||||
## Common Issues
|
||||
|
||||
**Docker daemon not accessible**
|
||||
```
|
||||
Error: docker daemon not accessible: Cannot connect to the Docker daemon
|
||||
```
|
||||
Solution: Check that Docker is running (`docker ps` works), your user is in the `docker` group, or set `DOCKER_HOST` if using remote Docker.
|
||||
|
||||
**No findings on known vulnerable containers**
|
||||
Solution: Check your `--severity` and `--target` flags. The default scans all targets at all severities. Use `--verbose` to see which analyzers run.
|
||||
|
||||
**Build fails with "module not found"**
|
||||
Solution: Run `go mod download` to fetch dependencies. Make sure you're using Go 1.23 or later (`go version`).
|
||||
|
||||
**SARIF file not showing in GitHub Security**
|
||||
Solution: Upload it in a GitHub Actions workflow using `github/codeql-action/upload-sarif@v2`. SARIF files don't auto-upload, they need explicit CI integration.
|
||||
|
||||
## Related Projects
|
||||
|
||||
If you found this interesting, check out:
|
||||
|
||||
- **Trivy** - comprehensive vulnerability scanner that includes configuration scanning. This project focuses specifically on runtime and build-time Docker security.
|
||||
- **Falco** - runtime security monitoring using eBPF. Complements this tool by detecting threats during execution.
|
||||
- **Docker Bench Security** - official Docker security checker. This project improves on it with structured output, CI/CD integration, and programmatic access.
|
||||
docker/ Docker SDK wrapper
|
||||
finding/ The Finding type and severity levels
|
||||
parser/ Dockerfile and compose file parsers
|
||||
|
|
@ -0,0 +1,553 @@
|
|||
# Core Security Concepts
|
||||
|
||||
This document explains the security concepts you'll encounter while building this project. These aren't just definitions. We'll dig into why they matter and how they actually work.
|
||||
|
||||
## Linux Capabilities
|
||||
|
||||
### What It Is
|
||||
|
||||
Linux capabilities split the monolithic root privilege into 41 discrete permissions. Instead of checking "is this process root?", the kernel checks "does this process have CAP_NET_ADMIN?" before allowing network configuration changes.
|
||||
|
||||
The capability model was introduced in kernel 2.2 (1999) to enable privilege separation. A web server binding to port 80 only needs `CAP_NET_BIND_SERVICE`, not full root access. A backup program only needs `CAP_DAC_READ_SEARCH` to read protected files.
|
||||
|
||||
### Why It Matters
|
||||
|
||||
Containers run with a default set of capabilities that enable common operations. Docker drops 13 dangerous capabilities by default but still grants 14 others. Adding back dropped capabilities or granting new ones can enable complete container escape.
|
||||
|
||||
When you run `docker run --privileged`, Docker grants all 41 capabilities. This is equivalent to running as root on the host, bypassing every container isolation mechanism.
|
||||
|
||||
### How It Works
|
||||
|
||||
Capabilities are stored as 64-bit bitmasks in the process credential structure. The kernel checks these bits before privileged operations. Here's how this project decodes them from `/proc/PID/status`:
|
||||
|
||||
```go
|
||||
// internal/proc/capabilities.go:87-112
|
||||
func (c *CapabilitySet) HasCapability(name string) bool {
|
||||
bit, ok := capabilityNames[name]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return (c.Effective & (1 << bit)) != 0
|
||||
}
|
||||
```
|
||||
|
||||
The `Effective` set determines what the process can currently do. Other sets (`Permitted`, `Inheritable`, `Bounding`, `Ambient`) control capability transitions across exec() and privilege changes.
|
||||
|
||||
The project maps all 41 capabilities to severity levels in `internal/rules/capabilities.go:24-323`:
|
||||
|
||||
```go
|
||||
"CAP_SYS_ADMIN": {
|
||||
Severity: finding.SeverityCritical,
|
||||
Description: "Perform a range of system administration operations...",
|
||||
}
|
||||
```
|
||||
|
||||
When scanning containers, the code at `internal/analyzer/container.go:76-102` iterates added capabilities and creates findings for dangerous ones:
|
||||
|
||||
```go
|
||||
for _, cap := range info.HostConfig.CapAdd {
|
||||
capName := strings.ToUpper(string(cap))
|
||||
capInfo, exists := rules.GetCapabilityInfo(capName)
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
if capInfo.Severity >= finding.SeverityHigh {
|
||||
// Create finding with severity from capability database
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Common Attacks
|
||||
|
||||
**Container escape via CAP_SYS_ADMIN**
|
||||
|
||||
This capability enables mounting filesystems. An attacker can mount the host's root filesystem inside the container:
|
||||
|
||||
```bash
|
||||
# Inside container with CAP_SYS_ADMIN
|
||||
mkdir /host
|
||||
mount /dev/sda1 /host
|
||||
chroot /host
|
||||
# Now running on the host
|
||||
```
|
||||
|
||||
The Shocker exploit (CVE-2014-6407) used `CAP_DAC_READ_SEARCH` similarly. It could open any file descriptor by path, bypassing namespace isolation.
|
||||
|
||||
**Process injection via CAP_SYS_PTRACE**
|
||||
|
||||
With this capability, a process can use ptrace() to attach to any other process, read its memory, and inject code:
|
||||
|
||||
```c
|
||||
ptrace(PTRACE_ATTACH, target_pid, NULL, NULL);
|
||||
ptrace(PTRACE_POKETEXT, target_pid, address, shellcode);
|
||||
ptrace(PTRACE_SETREGS, target_pid, NULL, ®s);
|
||||
ptrace(PTRACE_CONT, target_pid, NULL, NULL);
|
||||
```
|
||||
|
||||
An attacker in a container with `CAP_SYS_PTRACE` and `--pid=host` can inject into PID 1 on the host.
|
||||
|
||||
**Network manipulation via CAP_NET_ADMIN**
|
||||
|
||||
This capability allows modifying routing tables, firewall rules, and network namespaces. An attacker can:
|
||||
|
||||
- Redirect traffic destined for other containers
|
||||
- Create network tunnels to exfiltrate data
|
||||
- Modify iptables rules to bypass network policies
|
||||
- Escape to the host network namespace
|
||||
|
||||
### Defense Strategies
|
||||
|
||||
Drop all capabilities, then add back only what's needed:
|
||||
|
||||
```bash
|
||||
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
|
||||
```
|
||||
|
||||
This project detects when containers add dangerous capabilities. The severity comes from the capability database, not hardcoded logic. Adding a new risky capability just requires updating `internal/rules/capabilities.go`.
|
||||
|
||||
## Namespace Isolation
|
||||
|
||||
### What It Is
|
||||
|
||||
Linux namespaces provide separate views of system resources. Processes in different namespaces see different sets of PIDs, network interfaces, mount points, IPC objects, hostnames, users, and cgroups.
|
||||
|
||||
Docker creates 6 namespaces for each container by default:
|
||||
- **PID namespace** - isolates process IDs
|
||||
- **Network namespace** - isolates network interfaces
|
||||
- **Mount namespace** - isolates filesystem mount points
|
||||
- **IPC namespace** - isolates System V IPC and POSIX message queues
|
||||
- **UTS namespace** - isolates hostname and domain name
|
||||
- **User namespace** - isolates user and group IDs (optional, not default)
|
||||
|
||||
### Why It Matters
|
||||
|
||||
Sharing a namespace with the host breaks that isolation boundary. Running with `--pid=host` lets the container see all host processes. With `CAP_SYS_PTRACE`, it can inject into them.
|
||||
|
||||
The most dangerous is `--net=host`, which puts the container on the host network stack. The container can now bind to any host port, sniff all traffic, and reconfigure the host's network.
|
||||
|
||||
### How It Works
|
||||
|
||||
The kernel maintains a namespace inode for each namespace type. Processes belong to namespaces via these inodes. The `/proc/PID/ns/` directory exposes these as symlinks:
|
||||
|
||||
```bash
|
||||
$ ls -l /proc/self/ns/
|
||||
lrwxrwxrwx 1 user user 0 Jan 31 12:00 pid -> 'pid:[4026531836]'
|
||||
lrwxrwxrwx 1 user user 0 Jan 31 12:00 net -> 'net:[4026531840]'
|
||||
```
|
||||
|
||||
Containers in different namespaces have different inode numbers. The code at `internal/analyzer/container.go:125-161` checks if containers share host namespaces:
|
||||
|
||||
```go
|
||||
if info.HostConfig.NetworkMode == "host" {
|
||||
control, _ := benchmark.Get("5.9")
|
||||
f := finding.New("CIS-5.9", control.Title, finding.SeverityHigh, target)
|
||||
findings = append(findings, f)
|
||||
}
|
||||
```
|
||||
|
||||
Kubernetes manifests make this mistake often:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
hostNetwork: true # Shares host network namespace
|
||||
hostPID: true # Shares host PID namespace
|
||||
containers:
|
||||
- name: monitor
|
||||
image: debug-tools
|
||||
```
|
||||
|
||||
### Common Attacks
|
||||
|
||||
**Host process discovery via --pid=host**
|
||||
|
||||
With the host PID namespace, an attacker can enumerate all host processes, find sensitive services, and identify attack targets:
|
||||
|
||||
```bash
|
||||
# Inside container with --pid=host
|
||||
ps aux | grep -i password
|
||||
ls -la /proc/*/environ | xargs grep -a SECRET
|
||||
```
|
||||
|
||||
This exposes process command lines, environment variables, and open file descriptors.
|
||||
|
||||
**Network sniffing via --net=host**
|
||||
|
||||
On the host network namespace, the container can run packet capture tools to intercept traffic intended for other containers or the host:
|
||||
|
||||
```bash
|
||||
tcpdump -i eth0 -w capture.pcap
|
||||
# Exfiltrate capture.pcap later
|
||||
```
|
||||
|
||||
This bypasses Docker's network isolation completely.
|
||||
|
||||
### Defense Strategies
|
||||
|
||||
Never share host namespaces in production. The code at `internal/analyzer/container.go:145-161` flags all shared namespaces:
|
||||
|
||||
```go
|
||||
if info.HostConfig.PidMode == "host" {
|
||||
// Creates HIGH severity finding for CIS 5.15
|
||||
}
|
||||
if info.HostConfig.IpcMode == "host" {
|
||||
// Creates HIGH severity finding for CIS 5.16
|
||||
}
|
||||
```
|
||||
|
||||
For debugging, use `docker exec` to run tools in the container's namespaces rather than sharing the host's.
|
||||
|
||||
## Security Profiles (Seccomp, AppArmor, SELinux)
|
||||
|
||||
### What It Is
|
||||
|
||||
Security profiles enforce mandatory access control beyond traditional Unix permissions:
|
||||
|
||||
**Seccomp** (Secure Computing Mode) filters system calls using BPF (Berkeley Packet Filter) programs. It can block dangerous syscalls like `ptrace()`, `mount()`, and `reboot()`.
|
||||
|
||||
**AppArmor** restricts file access using path-based rules. A profile can allow reading `/etc/nginx/` but deny writing anywhere except `/var/log/nginx/`.
|
||||
|
||||
**SELinux** (Security-Enhanced Linux) enforces type-based mandatory access control. It uses security contexts like `system_u:object_r:httpd_sys_content_t:s0` to control access.
|
||||
|
||||
### Why It Matters
|
||||
|
||||
Without these profiles, a compromised container has full syscall access and can attempt container escape techniques. The default Docker seccomp profile blocks about 44 dangerous syscalls, but containers can run with `--security-opt seccomp=unconfined`.
|
||||
|
||||
### How It Works
|
||||
|
||||
Docker applies a default seccomp profile unless you override it. The profile is a JSON file defining allowed syscalls:
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultAction": "SCMP_ACT_ERRNO",
|
||||
"architectures": ["SCMP_ARCH_X86_64"],
|
||||
"syscalls": [
|
||||
{
|
||||
"names": ["read", "write", "open"],
|
||||
"action": "SCMP_ACT_ALLOW"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The code at `internal/analyzer/container.go:163-207` checks for missing or disabled profiles:
|
||||
|
||||
```go
|
||||
for _, opt := range info.HostConfig.SecurityOpt {
|
||||
if opt == "seccomp=unconfined" {
|
||||
// Creates HIGH severity finding
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
AppArmor profiles are text files in `/etc/apparmor.d/`:
|
||||
|
||||
```
|
||||
profile docker-default flags=(attach_disconnected,mediate_deleted) {
|
||||
deny @{PROC}/* w,
|
||||
deny /sys/[^f]*/** wklx,
|
||||
capability setuid,
|
||||
}
|
||||
```
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
**Disabling seccomp for convenience**
|
||||
|
||||
Developers disable seccomp when troubleshooting without understanding the risk:
|
||||
|
||||
```bash
|
||||
# Bad: Complete syscall access
|
||||
docker run --security-opt seccomp=unconfined app
|
||||
|
||||
# Good: Custom profile with needed syscalls
|
||||
docker run --security-opt seccomp=./custom-profile.json app
|
||||
```
|
||||
|
||||
The project detects this at `internal/analyzer/container.go:186-193`.
|
||||
|
||||
**Missing profiles entirely**
|
||||
|
||||
Older Docker versions or certain configurations don't apply profiles by default. The code checks both for explicit disabling and absence:
|
||||
|
||||
```go
|
||||
if !hasSeccomp && !info.HostConfig.Privileged {
|
||||
// Creates MEDIUM severity finding
|
||||
}
|
||||
```
|
||||
|
||||
### Defense Strategies
|
||||
|
||||
Always use security profiles in production. Start with Docker's defaults and customize only when needed. The project flags both disabled and missing profiles with different severities (HIGH for explicit disabling, MEDIUM for absence).
|
||||
|
||||
## Sensitive Path Mounts
|
||||
|
||||
### What It Is
|
||||
|
||||
Bind mounts expose host directories and files inside containers. Some paths give full host access if mounted, like `/var/run/docker.sock` (Docker control), `/proc` (process information), and `/` (entire filesystem).
|
||||
|
||||
### Why It Matters
|
||||
|
||||
The Docker socket is a UNIX socket that accepts Docker API commands. A container with this mounted can create new privileged containers, effectively escaping to the host:
|
||||
|
||||
```bash
|
||||
# Inside container with /var/run/docker.sock mounted
|
||||
docker run --privileged -v /:/host -it ubuntu chroot /host /bin/bash
|
||||
# Now on the host
|
||||
```
|
||||
|
||||
This is how the Tesla Kubernetes breach happened.
|
||||
|
||||
### How It Works
|
||||
|
||||
Docker bind mounts are specified with `-v` or `--mount`. The Docker API includes mount information in container inspection:
|
||||
|
||||
```json
|
||||
{
|
||||
"Mounts": [
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "/var/run/docker.sock",
|
||||
"Destination": "/var/run/docker.sock"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The code at `internal/analyzer/container.go:104-145` checks each mount against a database of 200+ dangerous paths defined in `internal/rules/paths.go`:
|
||||
|
||||
```go
|
||||
for _, mount := range info.Mounts {
|
||||
source := mount.Source
|
||||
if rules.IsDockerSocket(source) {
|
||||
// Creates CRITICAL severity finding
|
||||
}
|
||||
if rules.IsSensitivePath(source) {
|
||||
severity := rules.GetPathSeverity(source)
|
||||
// Creates finding with path-specific severity
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The rules database includes:
|
||||
|
||||
- Container runtime sockets (Docker, containerd, CRI-O)
|
||||
- System directories (`/proc`, `/sys`, `/dev`)
|
||||
- Configuration directories (`/etc`, `/root`)
|
||||
- Kubernetes secrets (`/var/lib/kubelet`, `/etc/kubernetes`)
|
||||
- Cloud provider credentials (`/root/.aws`, `/root/.kube`)
|
||||
- CI/CD agent data (`/var/lib/jenkins`, `/home/runner`)
|
||||
|
||||
Each path has a severity level and description in `internal/rules/paths.go:32-1100`.
|
||||
|
||||
### Common Attacks
|
||||
|
||||
**Docker socket escape**
|
||||
|
||||
This is the most common container escape technique:
|
||||
|
||||
```python
|
||||
# Python script inside container
|
||||
import docker
|
||||
client = docker.from_env() # Uses /var/run/docker.sock
|
||||
|
||||
# Create privileged container with host filesystem mounted
|
||||
container = client.containers.run(
|
||||
'ubuntu',
|
||||
'chroot /host /bin/bash',
|
||||
privileged=True,
|
||||
volumes={'/': {'bind': '/host'}},
|
||||
detach=True,
|
||||
stdin_open=True,
|
||||
tty=True
|
||||
)
|
||||
```
|
||||
|
||||
**Process memory access via /proc**
|
||||
|
||||
Mounting `/proc` exposes host process memory. Reading `/proc/PID/mem` can extract credentials, SSH keys, and other secrets from running processes.
|
||||
|
||||
**Kernel manipulation via /sys**
|
||||
|
||||
The `/sys` filesystem exposes kernel parameters. Writing to `/sys/kernel/` can disable security features, modify CPU settings, and trigger kernel vulnerabilities.
|
||||
|
||||
### Defense Strategies
|
||||
|
||||
Never mount the Docker socket unless the container specifically provides Docker-as-a-Service (like CI/CD runners). Even then, use alternatives like Docker's rootless mode or Kaniko for builds.
|
||||
|
||||
The project's path database is comprehensive. Adding a new dangerous path just requires an entry in `internal/rules/paths.go`.
|
||||
|
||||
## Secret Detection
|
||||
|
||||
### What It Is
|
||||
|
||||
Secrets are credentials embedded in Dockerfiles, environment variables, or docker-compose files. They include API keys, passwords, private keys, database URLs, and tokens.
|
||||
|
||||
### Why It Matters
|
||||
|
||||
Secrets in container images are easily discoverable. Anyone who can pull the image can extract the secrets:
|
||||
|
||||
```bash
|
||||
docker history image:tag
|
||||
docker inspect image:tag
|
||||
docker run image:tag env
|
||||
```
|
||||
|
||||
In 2019, researchers found over 100,000 Docker Hub images with leaked secrets. Many were active API keys for AWS, GitHub, and other services.
|
||||
|
||||
### How It Works
|
||||
|
||||
This project uses two techniques for secret detection:
|
||||
|
||||
**Pattern matching** with 80+ regular expressions in `internal/rules/secrets.go:29-821`:
|
||||
|
||||
```go
|
||||
{
|
||||
Type: SecretTypeAWSKey,
|
||||
Pattern: regexp.MustCompile(`(AKIA|ABIA|ACCA|ASIA)[0-9A-Z]{16}`),
|
||||
Description: "AWS Access Key ID",
|
||||
}
|
||||
```
|
||||
|
||||
**Entropy analysis** using Shannon entropy:
|
||||
|
||||
```go
|
||||
// internal/rules/secrets.go:910-925
|
||||
func CalculateEntropy(s string) float64 {
|
||||
freq := make(map[rune]float64)
|
||||
for _, c := range s {
|
||||
freq[c]++
|
||||
}
|
||||
length := float64(len(s))
|
||||
var entropy float64
|
||||
for _, count := range freq {
|
||||
p := count / length
|
||||
entropy -= p * math.Log2(p)
|
||||
}
|
||||
return entropy
|
||||
}
|
||||
```
|
||||
|
||||
Secrets typically have high entropy (>4.5 bits per character) because they're random. The code at `internal/analyzer/dockerfile.go:154-213` scans Dockerfiles:
|
||||
|
||||
```go
|
||||
if rules.IsSensitiveEnvName(varName) {
|
||||
// Check if variable name looks sensitive
|
||||
}
|
||||
if rules.IsHighEntropyString(varValue, 16, 4.5) {
|
||||
// Check if value has high entropy
|
||||
}
|
||||
secrets := rules.DetectSecrets(line)
|
||||
for _, secret := range secrets {
|
||||
// Create finding for each detected secret type
|
||||
}
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
The project detects:
|
||||
- AWS keys: `AKIA...`, `aws_secret_access_key=...`
|
||||
- GitHub tokens: `ghp_...`, `gho_...`
|
||||
- Google API keys: `AIza...`
|
||||
- Private keys: `-----BEGIN PRIVATE KEY-----`
|
||||
- JWTs: `eyJ...` (base64 encoded JSON)
|
||||
- Database URLs: `postgres://user:pass@host/db`
|
||||
- Generic patterns: `API_KEY=...`, `PASSWORD=...`
|
||||
|
||||
See `internal/rules/secrets.go:29-821` for the complete list.
|
||||
|
||||
### Defense Strategies
|
||||
|
||||
Use Docker secrets or environment variables at runtime:
|
||||
|
||||
```bash
|
||||
# Bad: Secret in Dockerfile
|
||||
ENV API_KEY=sk-abc123
|
||||
|
||||
# Good: Secret from environment
|
||||
docker run -e API_KEY=sk-abc123 app
|
||||
|
||||
# Better: Docker secrets (Swarm)
|
||||
echo "sk-abc123" | docker secret create api_key -
|
||||
docker service create --secret api_key app
|
||||
```
|
||||
|
||||
The project flags both hardcoded secrets and high-entropy strings in `ENV`, `ARG`, and `RUN` instructions.
|
||||
|
||||
## CIS Docker Benchmark
|
||||
|
||||
### What It Is
|
||||
|
||||
The CIS Docker Benchmark is a consensus-driven security configuration guide published by the Center for Internet Security. It provides 100+ controls across 7 sections:
|
||||
|
||||
1. Host Configuration
|
||||
2. Docker Daemon Configuration
|
||||
3. Docker Daemon Files and Directories
|
||||
4. Container Images and Build Files
|
||||
5. Container Runtime
|
||||
6. Docker Security Operations
|
||||
7. Docker Swarm Configuration
|
||||
|
||||
Each control has an ID (like "5.4"), title, description, and remediation steps. Controls are marked as "scored" (required for compliance) or "not scored" (recommended).
|
||||
|
||||
### Why It Matters
|
||||
|
||||
The benchmark represents industry best practices developed by security practitioners. Following it prevents common misconfigurations that lead to breaches.
|
||||
|
||||
Many compliance frameworks (PCI DSS, HIPAA, SOC 2) require following CIS benchmarks or equivalent standards. This project automates checking compliance.
|
||||
|
||||
### How It Works
|
||||
|
||||
The project registers all CIS controls in `internal/benchmark/controls.go:61-end`. Each control includes metadata:
|
||||
|
||||
```go
|
||||
Register(Control{
|
||||
ID: "5.4",
|
||||
Section: "Container Runtime",
|
||||
Title: "Ensure that privileged containers are not used",
|
||||
Description: "Using --privileged gives all capabilities to the container...",
|
||||
Remediation: "Do not run containers with --privileged flag...",
|
||||
Severity: finding.SeverityCritical,
|
||||
Scored: true,
|
||||
Level: 1,
|
||||
References: []string{"https://docs.docker.com/engine/reference/run/"},
|
||||
})
|
||||
```
|
||||
|
||||
Analyzers create findings that reference these controls:
|
||||
|
||||
```go
|
||||
control, _ := benchmark.Get("5.4")
|
||||
f := finding.New("CIS-5.4", control.Title, finding.SeverityCritical, target).
|
||||
WithDescription(control.Description).
|
||||
WithRemediation(control.Remediation).
|
||||
WithCISControl(control.ToCISControl())
|
||||
```
|
||||
|
||||
The `--cis` flag filters findings by control ID. The SARIF output includes CIS control IDs as tags for integration with compliance tools.
|
||||
|
||||
### Testing Your Understanding
|
||||
|
||||
Before moving to the architecture, make sure you can answer:
|
||||
|
||||
1. Why is `CAP_SYS_ADMIN` worse than `CAP_NET_RAW`? What specific attack does each enable?
|
||||
2. If a container shares the host PID namespace but has no special capabilities, can it still be dangerous? How?
|
||||
3. Why do we need both pattern matching and entropy analysis for secret detection? Give an example secret that each would catch.
|
||||
|
||||
If these questions feel unclear, re-read the relevant sections. The implementation will make more sense once these fundamentals click.
|
||||
|
||||
## Further Reading
|
||||
|
||||
**Essential:**
|
||||
- CIS Docker Benchmark v1.6.0 (PDF) - the complete specification this project implements
|
||||
- Linux Programmer's Manual capabilities(7) - authoritative capability documentation
|
||||
- Docker Security Documentation - official Docker security best practices
|
||||
|
||||
**Deep dives:**
|
||||
- "Understanding and Hardening Linux Containers" (NCC Group whitepaper) - container internals and escape techniques
|
||||
- "A Measurement Study of Docker Container Configurations on GitHub" (research paper) - data on real-world misconfigurations
|
||||
- Kernel source: security/capability.c - how capability checks actually work in the kernel
|
||||
|
||||
**Historical context:**
|
||||
- Original Docker security audit by ThoughtWorks (2014) - identified many issues this benchmark addresses
|
||||
- "Dirty COW" (CVE-2016-5195) - kernel vulnerability exploitable from containers
|
||||
- runc vulnerability CVE-2019-5736 - container escape via malicious image
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,339 +0,0 @@
|
|||
# Docker Security Concepts
|
||||
|
||||
This document explains the security concepts behind the checks that docksec performs. Understanding these helps you know why certain configurations are flagged and how to fix them properly.
|
||||
|
||||
## What is the CIS Docker Benchmark
|
||||
|
||||
The Center for Internet Security (CIS) publishes security configuration guides called benchmarks. The Docker Benchmark is a 150+ page document that defines security best practices for Docker deployments.
|
||||
|
||||
Each control has:
|
||||
- An ID (like "5.4" for privileged containers)
|
||||
- A title describing what to check
|
||||
- A rationale explaining why it matters
|
||||
- Remediation steps to fix issues
|
||||
- A severity level (scored vs unscored, Level 1 vs Level 2)
|
||||
|
||||
Level 1 controls are basic hardening that should apply to most environments. Level 2 controls provide stronger security but may break functionality.
|
||||
|
||||
docksec implements automated checks for controls that can be detected programmatically. Some controls require manual review (like "ensure the container host has been hardened").
|
||||
|
||||
## How Docker Isolation Works
|
||||
|
||||
Containers are not virtual machines. They share the host kernel. The isolation comes from Linux kernel features:
|
||||
|
||||
### Namespaces
|
||||
|
||||
Namespaces partition kernel resources so each container sees its own isolated copy:
|
||||
|
||||
| Namespace | What it isolates |
|
||||
|-----------|------------------|
|
||||
| PID | Process IDs (container sees itself as PID 1) |
|
||||
| NET | Network interfaces, routing tables, ports |
|
||||
| MNT | Filesystem mount points |
|
||||
| UTS | Hostname and domain name |
|
||||
| IPC | Inter-process communication (shared memory, semaphores) |
|
||||
| USER | User and group IDs |
|
||||
|
||||
When you run `docker run nginx`, Docker creates new namespaces. The nginx process inside cannot see host processes (PID namespace), cannot bind to host network interfaces (NET namespace), and gets its own filesystem view (MNT namespace).
|
||||
|
||||
Breaking namespace isolation is a container escape. This is why sharing host namespaces is dangerous:
|
||||
|
||||
```bash
|
||||
# Shares host PID namespace - container can see and signal all host processes
|
||||
docker run --pid=host nginx
|
||||
|
||||
# Shares host network - container has full network access, can bind any port
|
||||
docker run --network=host nginx
|
||||
```
|
||||
|
||||
### Control Groups (cgroups)
|
||||
|
||||
Cgroups limit and account for resource usage:
|
||||
|
||||
```bash
|
||||
# Limit memory to 512MB
|
||||
docker run --memory=512m nginx
|
||||
|
||||
# Limit to 0.5 CPU cores
|
||||
docker run --cpus=0.5 nginx
|
||||
|
||||
# Limit to 100 processes
|
||||
docker run --pids-limit=100 nginx
|
||||
```
|
||||
|
||||
Without limits, a container can consume all available resources (memory, CPU, disk I/O, PIDs) and crash the host or starve other containers. This is denial of service.
|
||||
|
||||
A fork bomb without PID limits:
|
||||
|
||||
```bash
|
||||
# Inside unlimited container
|
||||
:(){ :|:& };: # Creates processes until system dies
|
||||
```
|
||||
|
||||
With `--pids-limit=100`, it hits the limit and stops.
|
||||
|
||||
## Linux Capabilities
|
||||
|
||||
Root traditionally had all privileges. This is too coarse. Linux capabilities break root privileges into smaller units that can be granted independently.
|
||||
|
||||
Docker drops most capabilities by default. A container running as root inside still cannot:
|
||||
- Load kernel modules (CAP_SYS_MODULE)
|
||||
- Access raw network sockets for sniffing (CAP_NET_RAW)
|
||||
- Mount filesystems (requires CAP_SYS_ADMIN)
|
||||
|
||||
When you add capabilities back, you expand what the container can do:
|
||||
|
||||
```bash
|
||||
# Add ability to change any file ownership
|
||||
docker run --cap-add=CAP_CHOWN nginx
|
||||
|
||||
# Add network administration (modify routing, firewall, sniff traffic)
|
||||
docker run --cap-add=CAP_NET_ADMIN nginx
|
||||
```
|
||||
|
||||
Some capabilities are critical. Adding these is almost as bad as running privileged:
|
||||
|
||||
| Capability | What it allows |
|
||||
|------------|----------------|
|
||||
| CAP_SYS_ADMIN | Mount filesystems, namespace operations, many admin tasks |
|
||||
| CAP_SYS_PTRACE | Debug any process, read memory, inject code |
|
||||
| CAP_SYS_MODULE | Load kernel modules (instant root on host) |
|
||||
| CAP_NET_ADMIN | Full network control, MITM attacks |
|
||||
| CAP_DAC_OVERRIDE | Bypass all file permission checks |
|
||||
|
||||
docksec flags any container with these capabilities because they significantly weaken isolation.
|
||||
|
||||
### The Privileged Flag
|
||||
|
||||
`--privileged` gives all capabilities plus:
|
||||
- Access to all host devices
|
||||
- Disables seccomp and AppArmor
|
||||
- Removes cgroup restrictions
|
||||
|
||||
```bash
|
||||
docker run --privileged nginx
|
||||
```
|
||||
|
||||
This is essentially running on the host with root access. Common scenarios where people use it:
|
||||
- Running Docker inside Docker (DinD)
|
||||
- Accessing hardware devices
|
||||
- Debugging kernel issues
|
||||
|
||||
Most of these have safer alternatives. DinD can use `--privileged` on inner containers only. Device access can use `--device` to expose specific devices. Debugging should happen on test systems.
|
||||
|
||||
## Security Profiles: seccomp and AppArmor
|
||||
|
||||
### seccomp
|
||||
|
||||
Seccomp filters system calls. The Linux kernel has around 400 syscalls. Most programs only need a few dozen. Seccomp lets you block the rest.
|
||||
|
||||
Docker's default seccomp profile blocks dangerous syscalls:
|
||||
- `mount` (could escape container filesystem)
|
||||
- `reboot` (crash the host)
|
||||
- `kexec_load` (replace running kernel)
|
||||
- `bpf` (load arbitrary kernel code)
|
||||
|
||||
Disabling seccomp removes this protection:
|
||||
|
||||
```bash
|
||||
docker run --security-opt seccomp=unconfined nginx
|
||||
```
|
||||
|
||||
docksec flags `seccomp=unconfined` because it exposes the full syscall attack surface.
|
||||
|
||||
### AppArmor
|
||||
|
||||
AppArmor is a Mandatory Access Control (MAC) system. Unlike normal permissions (which processes can bypass if running as root), AppArmor rules apply regardless of privilege level.
|
||||
|
||||
Docker's default AppArmor profile restricts:
|
||||
- Writing to certain paths (`/proc`, `/sys`)
|
||||
- Mounting filesystems
|
||||
- Accessing raw network
|
||||
|
||||
Not having an AppArmor profile means relying only on discretionary controls, which privileged processes can bypass.
|
||||
|
||||
## Dangerous Mount Points
|
||||
|
||||
Mounting host paths into containers can break isolation:
|
||||
|
||||
### The Docker Socket
|
||||
|
||||
```bash
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock nginx
|
||||
```
|
||||
|
||||
The Docker socket gives full control over the Docker daemon. From inside the container:
|
||||
|
||||
```bash
|
||||
# Start a privileged container that mounts the host root
|
||||
docker run -v /:/host --privileged alpine chroot /host
|
||||
```
|
||||
|
||||
Game over. You have host root.
|
||||
|
||||
docksec flags Docker socket mounts as CRITICAL because they provide trivial container escape.
|
||||
|
||||
### Sensitive Host Paths
|
||||
|
||||
Some paths are always dangerous to mount:
|
||||
|
||||
| Path | Risk |
|
||||
|------|------|
|
||||
| `/` | Full host filesystem access |
|
||||
| `/etc` | Modify passwd, shadow, sudoers, cron |
|
||||
| `/var/run` | Access to sockets including Docker |
|
||||
| `/proc` | Kernel and process information, some writable |
|
||||
| `/sys` | Kernel configuration, some writable |
|
||||
| `/dev` | Device access |
|
||||
| `/boot` | Bootloader, kernel images |
|
||||
|
||||
Even read-only mounts of `/etc` expose sensitive data (password hashes, private keys).
|
||||
|
||||
## no-new-privileges
|
||||
|
||||
A process can gain privileges through:
|
||||
- setuid binaries (`sudo`, `passwd`)
|
||||
- setgid binaries
|
||||
- File capabilities
|
||||
|
||||
The `no-new-privileges` flag prevents this:
|
||||
|
||||
```bash
|
||||
docker run --security-opt=no-new-privileges nginx
|
||||
```
|
||||
|
||||
Even if an attacker compromises a container and finds a setuid binary, they cannot use it to escalate. This is defense in depth.
|
||||
|
||||
## Image Security
|
||||
|
||||
### Running as Root
|
||||
|
||||
By default, containers run as root (UID 0). This is root inside the container namespace. Without user namespace remapping, it maps to UID 0 on the host.
|
||||
|
||||
If an attacker escapes the container, they are root on the host.
|
||||
|
||||
Best practice is to create a non-root user:
|
||||
|
||||
```dockerfile
|
||||
RUN useradd -r -u 1000 appuser
|
||||
USER appuser
|
||||
```
|
||||
|
||||
docksec checks images for USER instructions and flags those running as root.
|
||||
|
||||
### Secrets in Images
|
||||
|
||||
Docker images are layers. Every instruction creates a layer. Layers are immutable and distributed.
|
||||
|
||||
```dockerfile
|
||||
ENV API_KEY=sk-secret-key-here
|
||||
```
|
||||
|
||||
This secret is baked into the image. Anyone who pulls the image can extract it:
|
||||
|
||||
```bash
|
||||
docker history --no-trunc myimage
|
||||
```
|
||||
|
||||
Even if you delete a secret in a later layer, the earlier layer still contains it:
|
||||
|
||||
```dockerfile
|
||||
COPY secrets.json /app/
|
||||
RUN rm /app/secrets.json # Still in previous layer!
|
||||
```
|
||||
|
||||
docksec checks for:
|
||||
- Secrets in ENV instructions
|
||||
- Secrets in ARG instructions
|
||||
- Known secret patterns in build commands
|
||||
|
||||
Use BuildKit secrets or runtime injection instead:
|
||||
|
||||
```dockerfile
|
||||
# BuildKit secret mount - not stored in image
|
||||
RUN --mount=type=secret,id=api_key cat /run/secrets/api_key
|
||||
```
|
||||
|
||||
### ADD vs COPY
|
||||
|
||||
```dockerfile
|
||||
ADD https://example.com/script.sh /app/
|
||||
ADD archive.tar.gz /app/
|
||||
```
|
||||
|
||||
ADD has implicit behaviors:
|
||||
- Fetches URLs (could be compromised)
|
||||
- Auto-extracts archives (zip bombs, symlink attacks)
|
||||
|
||||
COPY just copies files. What you see is what you get.
|
||||
|
||||
```dockerfile
|
||||
COPY script.sh /app/
|
||||
COPY archive.tar.gz /app/ # Copied as-is, not extracted
|
||||
```
|
||||
|
||||
docksec flags ADD usage because COPY is safer and more predictable.
|
||||
|
||||
## Network Security
|
||||
|
||||
### Inter-Container Communication
|
||||
|
||||
By default, containers on the same bridge network can communicate freely. Container A can connect to any port on Container B.
|
||||
|
||||
This matters when you run multiple applications. A compromised web container could attack your database container.
|
||||
|
||||
The `--icc=false` daemon flag disables this. Containers can only communicate through explicit links or published ports.
|
||||
|
||||
### Host Network Mode
|
||||
|
||||
```bash
|
||||
docker run --network=host nginx
|
||||
```
|
||||
|
||||
The container shares the host's network namespace. It can:
|
||||
- Bind to any port
|
||||
- See all network interfaces
|
||||
- Sniff traffic (with CAP_NET_RAW)
|
||||
|
||||
This breaks network isolation entirely. Normally used for performance sensitive applications or network tools.
|
||||
|
||||
## Compose File Considerations
|
||||
|
||||
Compose files can specify all these dangerous options:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
privileged: true # Full host access
|
||||
cap_add:
|
||||
- SYS_ADMIN # Mount filesystems, etc
|
||||
network_mode: host # No network isolation
|
||||
volumes:
|
||||
- /:/host # Full filesystem
|
||||
- /var/run/docker.sock:/var/run/docker.sock # Docker control
|
||||
```
|
||||
|
||||
docksec parses compose files and flags the same issues it finds in running containers. This catches problems before deployment.
|
||||
|
||||
## The Defense in Depth Model
|
||||
|
||||
No single control prevents all attacks. The goal is layered security:
|
||||
|
||||
1. **Namespace isolation** - Container cannot see host resources
|
||||
2. **Capability restrictions** - Container cannot perform privileged operations
|
||||
3. **seccomp filtering** - Container cannot make dangerous syscalls
|
||||
4. **AppArmor/SELinux** - Mandatory access control as backup
|
||||
5. **Resource limits** - Container cannot exhaust host resources
|
||||
6. **Non-root user** - Compromise gives limited privileges
|
||||
7. **Read-only filesystem** - Attacker cannot persist changes
|
||||
8. **No privileged flag** - None of the above is bypassed
|
||||
|
||||
docksec checks all these layers. A single CRITICAL finding (like privileged mode) can undermine everything else.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [CIS Docker Benchmark v1.6.0](https://www.cisecurity.org/benchmark/docker)
|
||||
- [Docker Security Documentation](https://docs.docker.com/engine/security/)
|
||||
- [Linux Capabilities Manual](https://man7.org/linux/man-pages/man7/capabilities.7.html)
|
||||
- [seccomp Documentation](https://docs.docker.com/engine/security/seccomp/)
|
||||
- [AppArmor Documentation](https://docs.docker.com/engine/security/apparmor/)
|
||||
Loading…
Reference in New Issue