AWS IAM Access Analyzer – automatyczny audyt bezpieczeństwa

AWS IAM Access Analyzer to narzędzie, które automatycznie wykrywa zasoby współdzielone z zewnętrznymi kontami. W tym tutorialu zbudujesz kompletny system audytu bezpieczeństwa z automatycznymi alertami używając tylko AWS CLI i bash. Pełna implementacja w mniej niż godzinę.

1. Wprowadzenie

Ile razy zastanawiałeś się: „Które z moich bucketów S3 są publicznie dostępne?” albo „Czy przypadkiem nie dałem komuś dostępu do mojej roli IAM?”. IAM Access Analyzer automatycznie znajduje takie problemy.

W tym artykule stworzymy system, który:

  • Automatycznie skanuje wszystkie zasoby w koncie AWS
  • Wykrywa nieautoryzowany dostęp zewnętrzny
  • Wysyła alerty na email przy znalezieniu problemu
  • Generuje raporty bezpieczeństwa

1.1. Co to jest IAM Access Analyzer?

IAM Access Analyzer używa automated reasoning (formalnej weryfikacji matematycznej) do analizy polityk zasobów. Sprawdza kto ma dostęp do twoich zasobów spoza twojego konta AWS lub organizacji.

Obsługiwane zasoby:

  • S3 Buckets
  • IAM Roles
  • KMS Keys
  • Lambda Functions
  • SQS Queues
  • Secrets Manager Secrets
  • SNS Topics

2. Wymagania wstępne

Przed rozpoczęciem upewnij się, że masz:

  • Konto AWS z uprawnieniami administratora
  • AWS CLI zainstalowane i skonfigurowane
  • jq zainstalowane (do przetwarzania JSON)

Instalacja AWS CLI:

# MacOS
brew install awscli

# Linux
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

# Windows
# Pobierz instalator z https://aws.amazon.com/cli/

Instalacja jq:

# MacOS
brew install jq

# Ubuntu/Debian
sudo apt-get install jq

# Amazon Linux
sudo yum install jq

Konfiguracja credentials:

aws configure
# AWS Access Key ID: [twój key]
# AWS Secret Access Key: [twój secret]
# Default region name: eu-central-1
# Default output format: json

3. Krok 1: Tworzenie Access Analyzer

aws accessanalyzer create-analyzer \
  --analyzer-name SecurityAuditAnalyzer \
  --type ACCOUNT \
  --region eu-central-1 \
  --tags Environment=Production,Purpose=SecurityAudit,ManagedBy=SecurityTeam

Sprawdź czy analyzer został utworzony:

aws accessanalyzer list-analyzers --region eu-central-1

Output:

{
    "analyzers": [
        {
            "arn": "arn:aws:access-analyzer:eu-central-1:123456789012:analyzer/SecurityAuditAnalyzer",
            "name": "SecurityAuditAnalyzer",
            "type": "ACCOUNT",
            "createdAt": "2025-02-06T10:30:00.000000+00:00",
            "status": "ACTIVE"
        }
    ]
}

💡 Tip: Access Analyzer jest darmowy! Płacisz tylko za API calls do innych serwisów AWS.

4. Krok 2: Tworzenie testowych zasobów

Stwórzmy kilka zasobów z różnymi poziomami dostępu, żeby zobaczyć jak działa Access Analyzer.

4.1. Publiczny bucket S3

# Utwórz bucket (zmień nazwę na unikalną)
BUCKET_NAME="test-public-bucket-$(date +%s)"

aws s3api create-bucket \
  --bucket $BUCKET_NAME \
  --region eu-central-1 \
  --create-bucket-configuration LocationConstraint=eu-central-1

# Utwórz publiczną politykę (NIE UŻYWAJ W PRODUKCJI!)
cat > /tmp/public-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::${BUCKET_NAME}/*"
  }]
}
EOF

# Zastosuj politykę
aws s3api put-bucket-policy \
  --bucket $BUCKET_NAME \
  --policy file:///tmp/public-policy.json

echo "Utworzono publiczny bucket: $BUCKET_NAME"

💡 Tip: Zapisz nazwę bucketa w zmiennej środowiskowej:

echo "export TEST_BUCKET=$BUCKET_NAME" >> ~/.bashrc
source ~/.bashrc

4.2. Rola IAM z cross-account access

# Utwórz trust policy dla zewnętrznego konta
cat > /tmp/trust-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "AWS": "arn:aws:iam::123456789012:root"
    },
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals": {
        "sts:ExternalId": "unique-external-id"
      }
    }
  }]
}
EOF

# Utwórz rolę
aws iam create-role \
  --role-name CrossAccountTestRole \
  --assume-role-policy-document file:///tmp/trust-policy.json \
  --description "Test role for Access Analyzer demo"

echo "Utworzono role z cross-account access"

Sprawdź utworzoną rolę:

aws iam get-role --role-name CrossAccountTestRole

5. Krok 3: Pobieranie findings

Access Analyzer automatycznie zacznie skanować zasoby po utworzeniu. Poczekaj 2-3 minuty, a potem sprawdźmy findings.

5.1. Podstawowe pobieranie

# Pobierz ID swojego konta
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

# Utwórz ARN analyzera
ANALYZER_ARN="arn:aws:access-analyzer:eu-central-1:${ACCOUNT_ID}:analyzer/SecurityAuditAnalyzer"

# Pobierz wszystkie findings
aws accessanalyzer list-findings \
  --analyzer-arn $ANALYZER_ARN \
  --region eu-central-1

Dla czytelniejszego outputu użyj JMESPath:

aws accessanalyzer list-findings \
  --analyzer-arn $ANALYZER_ARN \
  --region eu-central-1 \
  --query 'findings[].[resourceType, resource, status, principal]' \
  --output table

Output będzie wyglądał podobnie:

-----------------------------------------------------------------------
|                            ListFindings                              |
+------------------+----------------------------+--------+--------------+
|  AWS::S3::Bucket |  test-public-bucket-12345  | ACTIVE | {AWS: '*'}   |
|  AWS::IAM::Role  |  CrossAccountTestRole      | ACTIVE | {AWS: '...'} |
+------------------+----------------------------+--------+--------------+

5.2. Szczegóły konkretnego finding

# Pobierz ID pierwszego finding
FINDING_ID=$(aws accessanalyzer list-findings \
  --analyzer-arn $ANALYZER_ARN \
  --region eu-central-1 \
  --query 'findings[0].id' \
  --output text)

# Pobierz pełne szczegóły
aws accessanalyzer get-finding \
  --analyzer-arn $ANALYZER_ARN \
  --id $FINDING_ID \
  --region eu-central-1 \
  --output json | jq '.'

5.3. Filtrowanie findings

Znajdź tylko aktywne findings dla bucketów S3:

aws accessanalyzer list-findings \
  --analyzer-arn $ANALYZER_ARN \
  --region eu-central-1 \
  --filter '{
    "status": {"eq": ["ACTIVE"]},
    "resourceType": {"eq": ["AWS::S3::Bucket"]}
  }' \
  --query 'findings[].[resource, principal.AWS]' \
  --output table

Znajdź findings z ostatnich 24 godzin:

# Data 24h temu w formacie ISO8601
YESTERDAY=$(date -u -d '1 day ago' +%Y-%m-%dT%H:%M:%SZ)

aws accessanalyzer list-findings \
  --analyzer-arn $ANALYZER_ARN \
  --region eu-central-1 \
  --filter "{
    \"createdAt\": {
      \"gte\": \"$YESTERDAY\"
    }
  }" \
  --output json

Policz findings po typach zasobów:

aws accessanalyzer list-findings \
  --analyzer-arn $ANALYZER_ARN \
  --region eu-central-1 \
  --query 'findings[].resourceType' \
  --output text | sort | uniq -c

Output:

   1 AWS::IAM::Role
   1 AWS::S3::Bucket

6. Krok 4: Automatyczne alerty z SNS

Skonfigurujmy automatyczne powiadomienia email przy nowych findings.

6.1. Tworzenie SNS topic

# Utwórz SNS topic
TOPIC_ARN=$(aws sns create-topic \
  --name IAMSecurityAlerts \
  --region eu-central-1 \
  --query 'TopicArn' \
  --output text)

echo "SNS Topic utworzony: $TOPIC_ARN"

# Dodaj tagi
aws sns tag-resource \
  --resource-arn $TOPIC_ARN \
  --tags Key=Purpose,Value=SecurityAlerts Key=Team,Value=Security

# Subskrybuj swój email
EMAIL="twoj-email@example.com"  # ZMIEŃ NA SWÓJ EMAIL

aws sns subscribe \
  --topic-arn $TOPIC_ARN \
  --protocol email \
  --notification-endpoint $EMAIL

echo "Wyslano email konfirmacyjny na $EMAIL"
echo "Musisz potwierdzic subskrypcje klikajac link w emailu!"

Sprawdź subskrypcje:

aws sns list-subscriptions-by-topic \
  --topic-arn $TOPIC_ARN \
  --query 'Subscriptions[].[Endpoint, SubscriptionArn]' \
  --output table

Wyślij testową wiadomość:

aws sns publish \
  --topic-arn $TOPIC_ARN \
  --subject "Test - IAM Security Alert" \
  --message "To jest testowa wiadomość z systemu alertów IAM"

6.2. EventBridge rule dla automatycznych alertów

# Pobierz account ID
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

# Utwórz EventBridge rule
RULE_NAME="IAMAccessAnalyzerFindings"

aws events put-rule \
  --name $RULE_NAME \
  --description "Alert on new IAM Access Analyzer findings" \
  --event-pattern '{
    "source": ["aws.access-analyzer"],
    "detail-type": ["Access Analyzer Finding"],
    "detail": {
      "status": ["ACTIVE"]
    }
  }' \
  --state ENABLED \
  --region eu-central-1

echo "EventBridge Rule utworzona: $RULE_NAME"

# Dodaj SNS jako target z formatowaniem wiadomości
aws events put-targets \
  --rule $RULE_NAME \
  --region eu-central-1 \
  --targets '[
    {
      "Id": "1",
      "Arn": "'"$TOPIC_ARN"'",
      "InputTransformer": {
        "InputPathsMap": {
          "resource": "$.detail.resource",
          "resourceType": "$.detail.resourceType",
          "principal": "$.detail.principal.AWS",
          "findingId": "$.detail.id"
        },
        "InputTemplate": "\"Nowy IAM Finding wykryty!\\n\\nTyp zasobu: <resourceType>\\nZasób: <resource>\\nDostęp dla: <principal>\\nFinding ID: <findingId>\\n\\nSprawdź szczegóły w IAM Access Analyzer console.\""
      }
    }
  ]'

# Daj SNS permissions do EventBridge
cat > /tmp/sns-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Service": "events.amazonaws.com"
    },
    "Action": "SNS:Publish",
    "Resource": "$TOPIC_ARN",
    "Condition": {
      "ArnLike": {
        "aws:SourceArn": "arn:aws:events:eu-central-1:${ACCOUNT_ID}:rule/${RULE_NAME}"
      }
    }
  }]
}
EOF

aws sns set-topic-attributes \
  --topic-arn $TOPIC_ARN \
  --attribute-name Policy \
  --attribute-value file:///tmp/sns-policy.json

echo "EventBridge polaczony z SNS"
echo ""
echo "System alertow gotowy!"
echo "Teraz kazdy nowy finding wysle email na twoj adres."

Sprawdź czy rule działa:

aws events list-rules --region eu-central-1 | grep -A 5 $RULE_NAME

7. Krok 5: Generator raportu bezpieczeństwa

7.1. Prosty raport w terminalu

Stwórz skrypt bash generate_report.sh:

#!/bin/bash

# Kolory dla lepszej czytelności
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo "IAM Access Analyzer Security Report"
echo "========================================"
echo ""

# Pobierz dane
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ANALYZER_ARN="arn:aws:access-analyzer:eu-central-1:${ACCOUNT_ID}:analyzer/SecurityAuditAnalyzer"

echo "Account ID: $ACCOUNT_ID"
echo "Report Date: $(date '+%Y-%m-%d %H:%M:%S')"
echo ""

# Pobierz findings
FINDINGS=$(aws accessanalyzer list-findings \
  --analyzer-arn $ANALYZER_ARN \
  --region eu-central-1 \
  --output json)

# Policz findings
TOTAL=$(echo $FINDINGS | jq '.findings | length')
ACTIVE=$(echo $FINDINGS | jq '[.findings[] | select(.status=="ACTIVE")] | length')

echo "Summary:"
echo "--------"
echo -e "${YELLOW}Total Findings: $TOTAL${NC}"
echo -e "${RED}Active Findings: $ACTIVE${NC}"
echo ""

# Grupuj po typach
echo "Findings by Resource Type:"
echo "--------------------------"
echo $FINDINGS | jq -r '.findings[].resourceType' | sort | uniq -c | while read count type; do
    echo "  $type: $count"
done
echo ""

# Szczegóły findings
echo "Detailed Findings:"
echo "------------------"
echo $FINDINGS | jq -r '.findings[] | 
    "
Resource: \(.resource)
Type: \(.resourceType)
Status: \(.status)
Principal: \(.principal | tostring)
Created: \(.createdAt)
---"'

# Statystyki
echo ""
echo "Statistics:"
echo "-----------"
echo "S3 Buckets: $(echo $FINDINGS | jq '[.findings[] | select(.resourceType=="AWS::S3::Bucket")] | length')"
echo "IAM Roles: $(echo $FINDINGS | jq '[.findings[] | select(.resourceType=="AWS::IAM::Role")] | length')"
echo "Lambda Functions: $(echo $FINDINGS | jq '[.findings[] | select(.resourceType=="AWS::Lambda::Function")] | length')"
echo "KMS Keys: $(echo $FINDINGS | jq '[.findings[] | select(.resourceType=="AWS::KMS::Key")] | length')"

Nadaj uprawnienia i uruchom:

chmod +x generate_report.sh
./generate_report.sh

Zapisz raport do pliku:

./generate_report.sh > iam_security_report_$(date +%Y%m%d).txt

7.2. HTML raport

Stwórz skrypt generate_html_report.sh:

#!/bin/bash

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ANALYZER_ARN="arn:aws:access-analyzer:eu-central-1:${ACCOUNT_ID}:analyzer/SecurityAuditAnalyzer"
OUTPUT_FILE="iam_report_$(date +%Y%m%d_%H%M%S).html"

# Pobierz findings
FINDINGS=$(aws accessanalyzer list-findings \
  --analyzer-arn $ANALYZER_ARN \
  --region eu-central-1 \
  --output json)

TOTAL=$(echo $FINDINGS | jq '.findings | length')
ACTIVE=$(echo $FINDINGS | jq '[.findings[] | select(.status=="ACTIVE")] | length')

# Generuj HTML
cat > $OUTPUT_FILE <<EOF
<!DOCTYPE html>
<html>
<head>
    <title>IAM Access Analyzer Report</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
        h1 { color: #232f3e; }
        .summary { background: white; padding: 20px; border-radius: 8px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .finding { background: white; border-left: 4px solid #ff9900; padding: 15px; margin: 20px 0; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .critical { border-left-color: #d13212; }
        .resource-type { color: #232f3e; font-weight: bold; margin-top: 30px; }
        .timestamp { color: #666; font-size: 0.9em; }
        .stat { display: inline-block; margin: 10px 20px 10px 0; padding: 10px 15px; background: #e7f3ff; border-radius: 4px; }
        .stat-value { font-size: 24px; font-weight: bold; color: #0073bb; }
        .stat-label { font-size: 12px; color: #666; }
    </style>
</head>
<body>
    <h1>IAM Access Analyzer Security Report</h1>
    
    <div class="summary">
        <h2>Summary</h2>
        <p><strong>Account ID:</strong> $ACCOUNT_ID</p>
        <p><strong>Report Date:</strong> $(date '+%Y-%m-%d %H:%M:%S')</p>
        
        <div class="stat">
            <div class="stat-value">$TOTAL</div>
            <div class="stat-label">Total Findings</div>
        </div>
        <div class="stat">
            <div class="stat-value">$ACTIVE</div>
            <div class="stat-label">Active Findings</div>
        </div>
    </div>
EOF

# Dodaj findings
echo $FINDINGS | jq -r '.findings[] | @json' | while read -r finding; do
    RESOURCE=$(echo $finding | jq -r '.resource')
    TYPE=$(echo $finding | jq -r '.resourceType')
    STATUS=$(echo $finding | jq -r '.status')
    PRINCIPAL=$(echo $finding | jq -r '.principal | tostring')
    CREATED=$(echo $finding | jq -r '.createdAt')
    ACTIONS=$(echo $finding | jq -r '.action | join(", ")')
    
    cat >> $OUTPUT_FILE <<EOF
    <div class="finding $([ "$STATUS" = "ACTIVE" ] && echo "critical")">
        <p><strong>Resource:</strong> $RESOURCE</p>
        <p><strong>Type:</strong> $TYPE</p>
        <p><strong>Status:</strong> $STATUS</p>
        <p><strong>Principal:</strong> <pre>$PRINCIPAL</pre></p>
        <p><strong>Actions:</strong> $ACTIONS</p>
        <p class="timestamp">Created: $CREATED</p>
    </div>
EOF
done

# Zamknij HTML
cat >> $OUTPUT_FILE <<EOF
</body>
</html>
EOF

echo "Raport wygenerowany: $OUTPUT_FILE"
echo "Otworz plik w przegladarce:"
echo "  open $OUTPUT_FILE        # MacOS"
echo "  xdg-open $OUTPUT_FILE    # Linux"
echo "  start $OUTPUT_FILE       # Windows"

Nadaj uprawnienia i uruchom:

chmod +x generate_html_report.sh
./generate_html_report.sh

8. Best practices dla produkcji

8.1. Używaj organization-wide analyzer

Jeśli masz AWS Organizations, stwórz analyzer typu ORGANIZATION:

aws accessanalyzer create-analyzer \
  --analyzer-name OrgWideSecurityAnalyzer \
  --type ORGANIZATION \
  --region eu-central-1

8.2. Archiwizuj resolved findings

# Znajdź resolved findings
RESOLVED=$(aws accessanalyzer list-findings \
  --analyzer-arn $ANALYZER_ARN \
  --region eu-central-1 \
  --filter '{"status": {"eq": ["RESOLVED"]}}' \
  --query 'findings[].id' \
  --output text)

# Archiwizuj każdy
for FINDING_ID in $RESOLVED; do
    aws accessanalyzer update-findings \
        --analyzer-arn $ANALYZER_ARN \
        --ids $FINDING_ID \
        --status ARCHIVED \
        --region eu-central-1
    
    echo "Zarchiwizowano: $FINDING_ID"
done

8.3. Scheduled scans – cron job

Dodaj do crona codzienne raportowanie:

# Edytuj crontab
crontab -e

# Dodaj linię (codziennie o 9:00)
0 9 * * * /home/ec2-user/generate_report.sh && mail -s "Daily IAM Report" admin@example.com < /home/ec2-user/iam_security_report_$(date +\%Y\%m\%d).txt

8.4. CloudWatch metrics

# Policz aktywne findings
ACTIVE_COUNT=$(aws accessanalyzer list-findings \
  --analyzer-arn $ANALYZER_ARN \
  --region eu-central-1 \
  --filter '{"status": {"eq": ["ACTIVE"]}}' \
  --query 'length(findings)' \
  --output text)

# Wyślij do CloudWatch
aws cloudwatch put-metric-data \
  --namespace "IAM/Security" \
  --metric-name ActiveFindings \
  --value $ACTIVE_COUNT \
  --region eu-central-1

echo "Published metric: ActiveFindings = $ACTIVE_COUNT"

Stwórz CloudWatch Alarm:

aws cloudwatch put-metric-alarm \
  --alarm-name IAM-HighActiveFindings \
  --alarm-description "Alert when active findings exceed threshold" \
  --metric-name ActiveFindings \
  --namespace IAM/Security \
  --statistic Sum \
  --period 300 \
  --evaluation-periods 1 \
  --threshold 5 \
  --comparison-operator GreaterThanThreshold \
  --region eu-central-1

9. Czyszczenie zasobów

Stwórz skrypt cleanup.sh:

#!/bin/bash

echo "Rozpoczynam czyszczenie zasobow..."

# Pobierz dane
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ANALYZER_ARN="arn:aws:access-analyzer:eu-central-1:${ACCOUNT_ID}:analyzer/SecurityAuditAnalyzer"

# Usuń EventBridge Rules
echo "Usuwam EventBridge rules..."
RULE_NAME="IAMAccessAnalyzerFindings"

TARGETS=$(aws events list-targets-by-rule \
    --rule $RULE_NAME \
    --region eu-central-1 2>/dev/null | jq -r '.Targets[].Id')

if [ ! -z "$TARGETS" ]; then
    aws events remove-targets \
        --rule $RULE_NAME \
        --ids $TARGETS \
        --region eu-central-1 2>/dev/null
fi

aws events delete-rule \
    --name $RULE_NAME \
    --region eu-central-1 2>/dev/null

echo "  Usunieto EventBridge rule"

# Usuń SNS Topic
echo "Usuwam SNS topic..."
TOPICS=$(aws sns list-topics --query 'Topics[].TopicArn' --output text)
for TOPIC in $TOPICS; do
    if [[ $TOPIC == *"IAMSecurityAlerts"* ]]; then
        aws sns delete-topic --topic-arn $TOPIC
        echo "  Usunieto SNS topic"
    fi
done

# Usuń testowy bucket S3
echo "Usuwam testowe buckety S3..."
BUCKETS=$(aws s3api list-buckets \
    --query 'Buckets[?starts_with(Name, `test-public-bucket`)].Name' \
    --output text)

for BUCKET in $BUCKETS; do
    aws s3api delete-bucket-policy --bucket $BUCKET 2>/dev/null
    aws s3 rm s3://$BUCKET --recursive 2>/dev/null
    aws s3api delete-bucket --bucket $BUCKET 2>/dev/null
    echo "  Usunieto bucket: $BUCKET"
done

# Usuń testową rolę IAM
echo "Usuwam testowa role IAM..."
aws iam delete-role --role-name CrossAccountTestRole 2>/dev/null
echo "  Usunieto CrossAccountTestRole"

# Usuń Access Analyzer
echo "Usuwam Access Analyzer..."
aws accessanalyzer delete-analyzer \
    --analyzer-name SecurityAuditAnalyzer \
    --region eu-central-1 2>/dev/null
echo "  Usunieto Access Analyzer"

echo ""
echo "Cleanup complete!"

Nadaj uprawnienia i uruchom:

chmod +x cleanup.sh
./cleanup.sh

⚠️ Uwaga: Ten skrypt usuwa WSZYSTKIE zasoby utworzone w tym tutorialu!

10. Podsumowanie

W tym tutorialu stworzyliśmy kompletny system audytu bezpieczeństwa IAM używając tylko AWS CLI:

  • Skonfigurowaliśmy IAM Access Analyzer
  • Zbudowaliśmy automatyczne alerty przez SNS i EventBridge
  • Stworzyliśmy bash scripty do generowania raportów
  • Wdrożyliśmy monitoring z CloudWatch
  • Poznaliśmy best practices dla produkcji

Co dalej?

  • Zintegruj z Slack/Teams zamiast email
  • Dodaj webhook do Jiry dla ticketów
  • Stwórz dashboard w CloudWatch
  • Automatyzuj z Lambda dla remediacji

Przydatne linki

Related Posts