ЁЯПл The SchoolтА║ЁЯЫбя╕П SecurityтА║ЁЯз▒ рдзрдбрд╛ 08 тАФ IAM рдЖрдгрд┐ network рд╕реАрдорд╛: рдХрд┐рдорд╛рди рдЕрдзрд┐рдХрд╛рд░, рдмрдВрдж рджрд╛рд░реЗ
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯз▒ рдзрдбрд╛ 08 тАФ IAM рдЖрдгрд┐ network рд╕реАрдорд╛: рдХрд┐рдорд╛рди рдЕрдзрд┐рдХрд╛рд░, рдмрдВрдж рджрд╛рд░реЗ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 16 рдкреИрдХреА рдзрдбрд╛ 08 ┬╖ рдорд╛рдЧреАрд▓: lesson-07-secrets-kms ┬╖ рдкреБрдвреАрд▓: lesson-09-encryption


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ07, рдЖрдгрд┐ рджреЛрди рдкреНрд░рдХрд╛рд░рдЪреНрдпрд╛ рд╕реАрдорд╛. IAM: grades service рд▓рд╛ рдкрд╛рдЪ permissions рджрд┐рд▓реНрдпрд╛ рд╣реЛрддреНрдпрд╛ рдЖрдгрд┐ рддреА рджреЛрдирдЪ рд╡рд╛рдкрд░рдд рд╣реЛрддреА тАФ least privilege рд╕рд╛рдВрдЧрддреЗ рдХреА рдЙрд░рд▓реЗрд▓реНрдпрд╛ рддреАрди рдХрд╛рдвреВрди рдЯрд╛рдХрд╛. Network: security group рдлрдХреНрдд рдпрд╛рджреАрдд рдЕрд╕рд▓реЗрд▓реНрдпрд╛рдВрдирд╛рдЪ рдЖрдд рдпреЗрдК рджреЗрддреЛ, рдореНрд╣рдгреВрди database рдлрдХреНрдд app рд▓рд╛ рд╕реНрд╡реАрдХрд╛рд░рддреЛ, рдмрд╛рдХреА рдХреЛрдгрд╛рд▓рд╛рд╣реА рдирд╛рд╣реА. рддреНрдпрд╛рдВрдЪреНрдпрд╛ рднреЛрд╡рддреА: private subnets, NACLs, рд╕рдореЛрд░ рдПрдХ WAF, рдЖрдгрд┐ internet рд╡рд░реВрди SSH рдирд╛рд╣реА. sec/demo.py рдордзрд▓реЗ boundaries() рджреЛрдиреНрд╣реА рджрд╛рдЦрд╡рддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдкреНрд░рддреНрдпреЗрдХ рдХрд░реНрдордЪрд╛рд▒реНрдпрд╛рд▓рд╛ рдПрдХ рдЪрд╛рд╡реНрдпрд╛рдВрдЪрд╛ рдЬреБрдбрдЧрд╛ рдорд┐рд│рддреЛ. рдХреЕрдиреНрдЯреАрдирдордзрд▓реНрдпрд╛ рдирд╡реНрдпрд╛ рдорджрддрдиреАрд╕рдиреЗ "рд╕реБрд░рдХреНрд╖рд┐рдд рд░рд╛рд╣рд╛рд╡реЗ рдореНрд╣рдгреВрди рд╕рдЧрд│реНрдпрд╛ рдЪрд╛рд╡реНрдпрд╛" рдорд╛рдЧрд┐рддрд▓реНрдпрд╛. рддрд┐рд▓рд╛ рдХреЕрдиреНрдЯреАрдирдЪреА рдЪрд╛рд╡реА, рднрд╛рдВрдбрд╛рд░рдЦреЛрд▓реАрдЪреА рдЪрд╛рд╡реА, рдореБрдЦреНрдпрд╛рдзреНрдпрд╛рдкрдХрд╛рдВрдЪреНрдпрд╛ рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдЪреА рдЪрд╛рд╡реА, server-room рдЪреА рдЪрд╛рд╡реА рдЖрдгрд┐ рдкрд░реАрдХреНрд╖реЗрдЪреНрдпрд╛ рдХрдкрд╛рдЯрд╛рдЪреА рдЪрд╛рд╡реА рдорд┐рд│рд╛рд▓реА. рдПрдХрд╛ рдорд╣рд┐рдиреНрдпрд╛рдд рддрд┐рдиреЗ рджреЛрдирдЪ рд╡рд╛рдкрд░рд▓реНрдпрд╛: рдХреЕрдиреНрдЯреАрди рдЖрдгрд┐ рднрд╛рдВрдбрд╛рд░рдЦреЛрд▓реА.

рд╕реБрд░рдХреНрд╖рд╛ рдХрд╛рд░реНрдпрд╛рд▓рдп рджрд╛рд░рд╛рдВрдЪреА рдиреЛрдВрджрд╡рд╣реА рдкрд╛рд╣рддреЗ. "рддреБрдореНрд╣реА рджреЛрди рдЪрд╛рд╡реНрдпрд╛ рд╡рд╛рдкрд░рд▓реНрдпрд╛. рдЙрд░рд▓реЗрд▓реНрдпрд╛ рддреАрди рдЖрдореНрд╣реА рдкрд░рдд рдШреЗрддреЛ." рддрд┐рдЪрд╛ рдЬреБрдбрдЧрд╛ рдХрдзреА рдЪреЛрд░реАрд▓рд╛ рдЧреЗрд▓рд╛, рддрд░ рдЪреЛрд░ рдкрд╛рдЪ рдирд╛рд╣реА, рджреЛрдирдЪ рджрд╛рд░реЗ рдЙрдШрдбреВ рд╢рдХрддреЛ. рдпрд╛рд▓рд╛рдЪ least privilege (рдХрд┐рдорд╛рди рдЕрдзрд┐рдХрд╛рд░) рдореНрд╣рдгрддрд╛рдд.

рдХрд╛рд░реНрдпрд╛рд▓рдп рдкреНрд░рддреНрдпреЗрдХ рдЦреЛрд▓реАрд╡рд░ рдПрдХ рдкрд╣рд╛рд░реЗрдХрд░реА (security group) рд╕реБрджреНрдзрд╛ рдареЗрд╡рддреЗ. рдкрд░реАрдХреНрд╖реЗрдЪреНрдпрд╛ рдХрдкрд╛рдЯрд╛рдЪреНрдпрд╛ рдкрд╣рд╛рд░реЗрдХрд▒реНрдпрд╛рдХрдбреЗ рдПрдХ рдпрд╛рджреА рдЖрд╣реЗ: "рдлрдХреНрдд рдкрд░реАрдХреНрд╖рд╛ рдХрд╛рд░реНрдпрд╛рд▓рдп, рдЖрдгрд┐ рдлрдХреНрдд рдпрд╛ рдПрдХрд╛рдЪ рджрд╛рд░рд╛рддреВрди". рдпрд╛рджреАрдд рдирд╕рд▓реЗрд▓реНрдпрд╛ рдкреНрд░рддреНрдпреЗрдХрд╛рд▓рд╛ рдкрд░рдд рдкрд╛рдард╡рд▓реЗ рдЬрд╛рддреЗ тАФ рдХрдкрд╛рдЯ рдХреБрдареЗ рдЖрд╣реЗ рд╣реЗ рдорд╛рд╣реАрдд рдЕрд╕рд▓реЗ рддрд░реА. рдЬреНрдпрд╛ рд╡рд┐рднрд╛рдЧрд╛рдд рд╣реА рдХрдкрд╛рдЯреЗ рдЖрд╣реЗрдд рддреНрдпрд╛рд▓рд╛ рд░рд╕реНрддреНрдпрд╛рдХрдбреЗ рдЙрдШрдбрдгрд╛рд░реЗ рджрд╛рд░рдЪ рдирд╛рд╣реА (private subnet).

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    net["ЁЯМН internet"] --> waf["ЁЯз▒ WAF<br/>managed rules ┬╖ rate limit"]
    waf --> alb["тЪЦя╕П load balancer<br/>public subnet ┬╖ sg-alb"]
    subgraph priv["ЁЯФТ private subnets"]
      app["ЁЯзСтАНЁЯПл grades service<br/>sg-app ┬╖ port 8080 from sg-alb only"]
      db["ЁЯЧДя╕П database<br/>port 5432 from sg-app only"]
    end
    alb --> app
    app --> db
    net -.->|"refused: no SSH, no direct DB"| db
    iam["ЁЯкк IAM role<br/>granted 5 ┬╖ used 2 тЖТ remove 3"] -.-> app

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/security/lesson-diagrams.html#l08

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдзрдбреЗ 01тАУ07 рдордзрд▓реЗ рдкреНрд░рддреНрдпреЗрдХ рдирд┐рдпрдВрддреНрд░рдг рдПрдХрджрд╛ рддрд░реА рдЪреБрдХреВ рд╢рдХрддреЗ. рдПрдХ рдЪреВрдХ рдХрд┐рддреА рджреВрд░ рдЬрд╛рддреЗ рд╣реЗ рд╕реАрдорд╛ рдард░рд╡рддрд╛рдд. dynamodb:* рдЕрд╕рд▓реЗрд▓рд╛ role рдПрдХрд╛ SSRF bug рдЪреЗ рд░реВрдкрд╛рдВрддрд░ delete рдЭрд╛рд▓реЗрд▓реНрдпрд╛ table рдордзреНрдпреЗ рдХрд░рддреЛ. 0.0.0.0/0 рд╡рд░реВрди connections рд╕реНрд╡реАрдХрд╛рд░рдгрд╛рд░рд╛ database рдПрдХрд╛ leak рдЭрд╛рд▓реЗрд▓реНрдпрд╛ password рдЪреЗ рд░реВрдкрд╛рдВрддрд░ рд╕рдВрдкреВрд░реНрдг data рдЪреНрдпрд╛ copy рдордзреНрдпреЗ рдХрд░рддреЛ. рдЫреЛрдЯреНрдпрд╛ permissions рдЖрдгрд┐ рдмрдВрдж рджрд╛рд░реЗ рдкреНрд░рддреНрдпреЗрдХ рдЪреВрдХ рдЫреЛрдЯреА рдареЗрд╡рддрд╛рдд. Auditor рд╕реБрджреНрдзрд╛ рдЖрдзреА рд╣реЗрдЪ рдкрд╛рд╣рд╛рдпрд▓рд╛ рдорд╛рдЧрддреЛ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

sec/cloud.py рдордзреНрдпреЗ: unused_permissions(granted, used) рдХреНрд░рдорд╡рд╛рд░ рд▓рд╛рд╡рд▓реЗрд▓рд╛ set difference рдкрд░рдд рдХрд░рддреЗ. reachable(rules, source, port) рдлрдХреНрдд-allow security group рдЪреЗ model рдЖрд╣реЗ: рдПрдЦрд╛рджрд╛ source рддреЗрд╡реНрд╣рд╛рдЪ рдЖрдд рдпреЗрддреЛ рдЬреЗрд╡реНрд╣рд╛ рдХреЛрдгрддрд╛ рддрд░реА рдирд┐рдпрдо рддреЛ source (рдХрд┐рдВрд╡рд╛ 0.0.0.0/0) рдЖрдгрд┐ рддреЛ port рджреЛрдиреНрд╣реА рдХрд╡реНрд╣рд░ рдХрд░рддреЛ. рддреЗ security-group рдЪреЗ рдирд╛рд╡ рдиреЗрдордХреЗ рдЬреБрд│рд╡рддреЗ, рдЖрдгрд┐ IP address рд▓рд╛ CIDR range рд╢реА рдЬреБрд│рд╡рддреЗ (10.20.3.4 рд╣рд╛ 10.20.0.0/16 рдЪреНрдпрд╛ рдЖрдд рдЖрд╣реЗ). sec/demo.py рдордзрд▓реЗ boundaries() рдЪрд╛рд░ sources рдПрдХрд╛ рдЕрд╢рд╛ SG рд╡рд┐рд░реБрджреНрдз рддрдкрд╛рд╕рддреЗ рдЬреЛ sg-alb тЖТ 8080 рдЖрдгрд┐ 10.20.0.0/16 тЖТ 5432 рд▓рд╛ рдкрд░рд╡рд╛рдирдЧреА рджреЗрддреЛ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 sec/demo.py boundaries
python3 - <<'EOF'
import sys; sys.path.insert(0, "sec"); from cloud import unused_permissions, reachable
granted = ["s3:GetObject", "s3:PutObject", "s3:ListBucket", "kms:Decrypt", "iam:PassRole", "sqs:*"]
used = ["s3:GetObject", "kms:Decrypt"]
print("remove:", unused_permissions(granted, used))
db_sg = [("sg-app", 5432)]
for src, port in (("sg-app", 5432), ("sg-alb", 5432), ("0.0.0.0/0", 5432), ("sg-app", 22)):
    print(f"database SG: {src:<10} тЖТ {port:<5} {'allowed' if reachable(db_sg, src, port) else 'refused'}")
open_sg = [("0.0.0.0/0", 22)]
print("a rule for 0.0.0.0/0 on 22 lets in 203.0.113.7 тЖТ", reachable(open_sg, "203.0.113.7", 22))
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

boundaries рд╣реЗ рдЫрд╛рдкрддреЗ:

тФАтФА the grades service was granted 5 permissions and used 2 тЖТ remove ['dynamodb:*', 'ec2:*', 's3:DeleteObject']
   sg-alb         тЖТ port 8080  allowed
   0.0.0.0/0      тЖТ port 8080  refused
   10.20.0.0/16   тЖТ port 5432  allowed
   198.51.100.9   тЖТ port 22    refused
   private subnets for apps and data ┬╖ WAF in front ┬╖ no SSH from the internet ┬╖ least privilege everywhere

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

remove: ['iam:PassRole', 's3:ListBucket', 's3:PutObject', 'sqs:*']
database SG: sg-app     тЖТ 5432  allowed
database SG: sg-alb     тЖТ 5432  refused
database SG: 0.0.0.0/0  тЖТ 5432  refused
database SG: sg-app     тЖТ 22    refused
a rule for 0.0.0.0/0 on 22 lets in 203.0.113.7 тЖТ True

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

Least-privilege diff рдиреЗ iam:PassRole рдХрд╛рдврд▓реА тАФ рд╣реА permission рдирд╡реНрдпрд╛ resource рд▓рд╛ рдЬрд╛рд╕реНрдд рддрд╛рдХрджреАрдЪрд╛ role рджреЗрдК рд╢рдХрддреЗ тАФ рдЖрдгрд┐ рдПрдХ wildcard, sqs:*. Database рдЪрд╛ security group рдлрдХреНрдд app рд▓рд╛, рдлрдХреНрдд 5432 рд╡рд░ рдЖрдд рдпреЗрдК рджреЗрддреЛ: load balancer рд╕реБрджреНрдзрд╛ рдирд╛рдХрд╛рд░рд▓рд╛ рдЬрд╛рддреЛ, рдЖрдгрд┐ app рдХрдбреВрди SSH рд╕реБрджреНрдзрд╛. рдЖрдгрд┐ port 22 рд╡рд░рдЪрд╛ рдПрдХ рдирд┐рд╖реНрдХрд╛рд│рдЬреА 0.0.0.0/0 рдирд┐рдпрдо internet рд╡рд░рдЪреНрдпрд╛ рдХреЛрдгрддреНрдпрд╛рд╣реА address рд▓рд╛ рдЖрдд рдпреЗрдК рджреЗрддреЛ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ IAM Access Analyzer рд▓рд╛ рдкреВрд░реНрдг account рдордзрд▓рд╛ рди рд╡рд╛рдкрд░рд▓реЗрд▓рд╛ access рд╢реЛрдзреВ рджреНрдпрд╛, рдордЧ findings рд╡рд╛рдЪрд╛:

aws accessanalyzer create-analyzer --analyzer-name school-unused \
    --type ACCOUNT_UNUSED_ACCESS --configuration '{"unusedAccess": {"unusedAccessAge": 90}}'
aws accessanalyzer list-findings-v2 --analyzer-arn <analyzer-arn> \
    --filter '{"findingType": {"eq": ["UnusedPermission"]}}'

Policy attach рдХрд░рдгреНрдпрд╛рдЖрдзреА рддрдкрд╛рд╕рд╛ тАФ Access Analyzer рдЪреЗ policy validation wildcards рдЖрдгрд┐ рдзреЛрдХрд╛рджрд╛рдпрдХ рдЬреЛрдбреНрдпрд╛ flag рдХрд░рддреЗ:

aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://grades-service.json

Grades service рд╕рд╛рдареА least-privilege policy тАФ рджреЛрди actions, рдПрдХ bucket prefix, рдПрдХ key:

{
  "Version": "2012-10-17",
  "Statement": [
    {"Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::school-grades/reports/*"},
    {"Effect": "Allow", "Action": "kms:Decrypt", "Resource": "arn:aws:kms:ap-south-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"}
  ]
}

рдПрдХрдореЗрдХрд╛рдВрдХрдбреЗ рдирд┐рд░реНрджреЗрд╢ рдХрд░рдгрд╛рд░реЗ security groups тАФ database рдлрдХреНрдд app рдЪрд╛ group рд╕реНрд╡реАрдХрд╛рд░рддреЛ:

aws ec2 authorize-security-group-ingress --group-id sg-0db0000000000000a \
    --protocol tcp --port 5432 --source-group sg-0app000000000000b
aws ec2 authorize-security-group-ingress --group-id sg-0app000000000000b \
    --protocol tcp --port 8080 --source-group sg-0alb000000000000c

AWS common rule set рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ IP рд╕рд╛рдареА rate limit рдЕрд╕рд▓реЗрд▓реЗ WAF web ACL (Terraform):

resource "aws_wafv2_web_acl" "school" {
  name  = "school-web"
  scope = "REGIONAL"
  default_action {
    allow {}
  }
  rule {
    name     = "aws-common"
    priority = 1
    override_action {
      none {}
    }
    statement {
      managed_rule_group_statement {
        vendor_name = "AWS"
        name        = "AWSManagedRulesCommonRuleSet"
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "aws-common"
      sampled_requests_enabled   = true
    }
  }
  rule {
    name     = "rate-limit"
    priority = 2
    action {
      block {}
    }
    statement {
      rate_based_statement {
        limit              = 1000
        aggregate_key_type = "IP"
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "rate-limit"
      sampled_requests_enabled   = true
    }
  }
  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "school-web"
    sampled_requests_enabled   = true
  }
}

рдЖрдгрд┐ SSH рдРрд╡рдЬреА, рдХреЛрдгрддрд╛рд╣реА port рди рдЙрдШрдбрддрд╛, рдиреЛрдВрдж рдареЗрд╡рдгрд╛рд░рд╛ shell:

aws ssm start-session --target i-0123456789abcdef0

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдкреНрд░рддреНрдпреЗрдХ service рд▓рд╛ рд╕реНрд╡рддрдГрдЪрд╛ role рджреНрдпрд╛, рд╢реВрдиреНрдп permissions рдкрд╛рд╕реВрди рд╕реБрд░реБрд╡рд╛рдд рдХрд░рд╛ рдЖрдгрд┐ tests рдирд╛ рд▓рд╛рдЧрддреАрд▓ рддреЗрд╡рдвреНрдпрд╛рдЪ рдЬреЛрдбрд╛. Access Analyzer findings рджрд░ рдорд╣рд┐рдиреНрдпрд╛рд▓рд╛ рддрдкрд╛рд╕рд╛. Apps рдЖрдгрд┐ data private subnets рдордзреНрдпреЗ рдареЗрд╡рд╛, security groups address ranges рдРрд╡рдЬреА рдПрдХрдореЗрдХрд╛рдВрдХрдбреЗ рдирд┐рд░реНрджреЗрд╢рд┐рдд рдХрд░рд╛, рдЖрдгрд┐ port 22 рд╕рдЧрд│реАрдХрдбреЗ рдмрдВрдж рдХрд░рд╛.

тПня╕П рдкреБрдвреЗ

рджрд╛рд░реЗ рдмрдВрдж рдЖрд╣реЗрдд рдЖрдгрд┐ рдЪрд╛рд╡реНрдпрд╛ рдереЛрдбреНрдпрд╛рдЪ рдЖрд╣реЗрдд. рдЗрдорд╛рд░рдд рдЦрд┐рдбрдХреАрд╕рд╛рдареА рдПрдХ рдкреНрд░рд╢реНрди рдЙрд░рд▓рд╛: data рдЦреЛрд▓реНрдпрд╛рдВрдордзреВрди рдкреНрд░рд╡рд╛рд╕ рдХрд░рддрд╛рдирд╛, рдЖрдгрд┐ disk рд╡рд░ рдкрдбреВрди рдЕрд╕рддрд╛рдирд╛ рдХреБрд▓реВрдкрдмрдВрдж рдЖрд╣реЗ рдХрд╛? рдкреБрдвреЗ: рдкреНрд░рд╡рд╛рд╕рд╛рддрд▓реЗ рдЖрдгрд┐ рд╕рд╛рдард╡рд▓реЗрд▓реЗ encryption.

git checkout lesson-09-encryption

ЁЯз▒ Lesson 08 тАФ IAM and network boundaries: least privilege, closed doors

ЁЯУН You are here: Lesson 08 of 16 ┬╖ Previous: lesson-07-secrets-kms ┬╖ Next: lesson-09-encryption


ЁЯУж What's in this branch

Lessons 01тАУ07, plus two kinds of boundary. IAM: the grades service was given five permissions and used two тАФ least privilege says remove the other three. Network: a security group lets in only what is listed, so the database accepts the app and nothing else. Around them: private subnets, NACLs, a WAF in front, and no SSH from the internet. boundaries() in sec/demo.py shows both.

ЁЯзТ Explain like I'm 5

Every staff member gets a key ring. The new canteen helper asked for "all the keys, to be safe". She got the canteen key, the store-room key, the head's office key, the server-room key and the exam cupboard key. In a month she used two: the canteen and the store room.

The security office looks at the door log. "You used two keys. We take back the other three." If her ring is ever stolen, the thief opens two doors, not five. That is least privilege.

The office also puts a guard at each room (a security group). The exam cupboard's guard has a list: "only the exam office, only through this one door". Anyone not on the list is turned away тАФ even if they know where the cupboard is. The whole wing where the cupboards are has no door to the street at all (a private subnet).

ЁЯЧ║я╕П Diagram

flowchart LR
    net["ЁЯМН internet"] --> waf["ЁЯз▒ WAF<br/>managed rules ┬╖ rate limit"]
    waf --> alb["тЪЦя╕П load balancer<br/>public subnet ┬╖ sg-alb"]
    subgraph priv["ЁЯФТ private subnets"]
      app["ЁЯзСтАНЁЯПл grades service<br/>sg-app ┬╖ port 8080 from sg-alb only"]
      db["ЁЯЧДя╕П database<br/>port 5432 from sg-app only"]
    end
    alb --> app
    app --> db
    net -.->|"refused: no SSH, no direct DB"| db
    iam["ЁЯкк IAM role<br/>granted 5 ┬╖ used 2 тЖТ remove 3"] -.-> app

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/security/lesson-diagrams.html#l08

тЭУ What

ЁЯдФ Why

Because every control in lessons 01тАУ07 can be missed once. Boundaries decide how far a single mistake travels. A role with dynamodb:* turns an SSRF bug into a deleted table. A database that accepts connections from 0.0.0.0/0 turns one leaked password into a full copy of the data. Small permissions and closed doors make each mistake small. They are also what an auditor asks to see first.

ЁЯФз How (in this repo)

In sec/cloud.py: unused_permissions(granted, used) returns the sorted set difference. reachable(rules, source, port) models an allow-only security group: a source gets in only if some rule covers that source (or is 0.0.0.0/0) and that port. It matches a security-group name exactly, and an IP address against a CIDR range (10.20.3.4 is inside 10.20.0.0/16). boundaries() in sec/demo.py checks four sources against an SG that allows sg-alb тЖТ 8080 and 10.20.0.0/16 тЖТ 5432.

ЁЯзк Try it

python3 sec/demo.py boundaries
python3 - <<'EOF'
import sys; sys.path.insert(0, "sec"); from cloud import unused_permissions, reachable
granted = ["s3:GetObject", "s3:PutObject", "s3:ListBucket", "kms:Decrypt", "iam:PassRole", "sqs:*"]
used = ["s3:GetObject", "kms:Decrypt"]
print("remove:", unused_permissions(granted, used))
db_sg = [("sg-app", 5432)]
for src, port in (("sg-app", 5432), ("sg-alb", 5432), ("0.0.0.0/0", 5432), ("sg-app", 22)):
    print(f"database SG: {src:<10} тЖТ {port:<5} {'allowed' if reachable(db_sg, src, port) else 'refused'}")
open_sg = [("0.0.0.0/0", 22)]
print("a rule for 0.0.0.0/0 on 22 lets in 203.0.113.7 тЖТ", reachable(open_sg, "203.0.113.7", 22))
EOF

тЬЕ Verify тАФ what you should see

boundaries prints:

тФАтФА the grades service was granted 5 permissions and used 2 тЖТ remove ['dynamodb:*', 'ec2:*', 's3:DeleteObject']
   sg-alb         тЖТ port 8080  allowed
   0.0.0.0/0      тЖТ port 8080  refused
   10.20.0.0/16   тЖТ port 5432  allowed
   198.51.100.9   тЖТ port 22    refused
   private subnets for apps and data ┬╖ WAF in front ┬╖ no SSH from the internet ┬╖ least privilege everywhere

Your snippet prints:

remove: ['iam:PassRole', 's3:ListBucket', 's3:PutObject', 'sqs:*']
database SG: sg-app     тЖТ 5432  allowed
database SG: sg-alb     тЖТ 5432  refused
database SG: 0.0.0.0/0  тЖТ 5432  refused
database SG: sg-app     тЖТ 22    refused
a rule for 0.0.0.0/0 on 22 lets in 203.0.113.7 тЖТ True

ЁЯПБ What you just proved

The least-privilege diff removed iam:PassRole тАФ a permission that can hand a stronger role to a new resource тАФ and a wildcard, sqs:*. The database's security group lets in only the app, only on 5432: even the load balancer is refused, and so is SSH from the app. And one careless 0.0.0.0/0 rule on port 22 lets in any address on the internet.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ let IAM Access Analyzer find unused access across the account, then read the findings:

aws accessanalyzer create-analyzer --analyzer-name school-unused \
    --type ACCOUNT_UNUSED_ACCESS --configuration '{"unusedAccess": {"unusedAccessAge": 90}}'
aws accessanalyzer list-findings-v2 --analyzer-arn <analyzer-arn> \
    --filter '{"findingType": {"eq": ["UnusedPermission"]}}'

Check a policy before you attach it тАФ Access Analyzer's policy validation flags wildcards and risky combinations:

aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://grades-service.json

A least-privilege policy for the grades service тАФ two actions, one bucket prefix, one key:

{
  "Version": "2012-10-17",
  "Statement": [
    {"Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::school-grades/reports/*"},
    {"Effect": "Allow", "Action": "kms:Decrypt", "Resource": "arn:aws:kms:ap-south-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"}
  ]
}

Security groups that point at each other тАФ the database accepts only the app's group:

aws ec2 authorize-security-group-ingress --group-id sg-0db0000000000000a \
    --protocol tcp --port 5432 --source-group sg-0app000000000000b
aws ec2 authorize-security-group-ingress --group-id sg-0app000000000000b \
    --protocol tcp --port 8080 --source-group sg-0alb000000000000c

A WAF web ACL with the AWS common rule set and a rate limit per IP (Terraform):

resource "aws_wafv2_web_acl" "school" {
  name  = "school-web"
  scope = "REGIONAL"
  default_action {
    allow {}
  }
  rule {
    name     = "aws-common"
    priority = 1
    override_action {
      none {}
    }
    statement {
      managed_rule_group_statement {
        vendor_name = "AWS"
        name        = "AWSManagedRulesCommonRuleSet"
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "aws-common"
      sampled_requests_enabled   = true
    }
  }
  rule {
    name     = "rate-limit"
    priority = 2
    action {
      block {}
    }
    statement {
      rate_based_statement {
        limit              = 1000
        aggregate_key_type = "IP"
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "rate-limit"
      sampled_requests_enabled   = true
    }
  }
  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "school-web"
    sampled_requests_enabled   = true
  }
}

And instead of SSH, a logged shell with no open port:

aws ssm start-session --target i-0123456789abcdef0

ЁЯПн Why this matters in production: give each service its own role, start from zero permissions and add what the tests need. Review Access Analyzer findings every month. Keep apps and data in private subnets, point security groups at each other instead of at address ranges, and close port 22 everywhere.

тПня╕П Next

The doors are closed and the keys are few. One question left for the building desk: is the data locked while it travels between rooms, and while it sits on disk? Next: encryption in transit and at rest.

git checkout lesson-09-encryption
тЖР Previoussecrets kmsNext тЖТencryption

This page is the lesson's README from the lesson-08-iam-network branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.