ЁЯПл The SchoolтА║ЁЯПл VPCтА║ЁЯзН рдзрдбрд╛ 06 тАФ Security groups рд╡рд┐рд░реБрджреНрдз NACLs: рдкрд╛рд╣реБрдгреНрдпрд╛рдВрдЪреА рдпрд╛рджреА рдЖрдгрд┐ wing рдЪреЗ рджрд╛рд░
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯзН рдзрдбрд╛ 06 тАФ Security groups рд╡рд┐рд░реБрджреНрдз NACLs: рдкрд╛рд╣реБрдгреНрдпрд╛рдВрдЪреА рдпрд╛рджреА рдЖрдгрд┐ wing рдЪреЗ рджрд╛рд░

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 06 ┬╖ рдорд╛рдЧреЗ: lesson-05-igw-public ┬╖ рдкреБрдвреЗ: lesson-07-endpoints


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

рдзрдбреЗ 01тАУ05, рдЖрдгрд┐ campus рдЪреНрдпрд╛ рдЖрдд рдкреНрд░рддреНрдпреЗрдХ packet рд▓рд╛ рднреЗрдЯрдгрд╛рд░реЗ рджреЛрди рд░рдХреНрд╖рдХ: vpc/network.py рдордзреАрд▓ SG (desk рд╡рд░рдЪреА рдкрд╛рд╣реБрдгреНрдпрд╛рдВрдЪреА рдпрд╛рджреА тАФ stateful) рдЖрдгрд┐ NACL (wing рдЪреЗ рджрд╛рд░ тАФ stateless, рдХреНрд░рдорд╛рдВрдХрд┐рдд). vpc/demo.py рдордзреАрд▓ guards() рддреАрди packets рджреЛрдиреНрд╣реАрдВрдордзреВрди рдкрд╛рдпрд░реА-рдкрд╛рдпрд░реАрдиреЗ рдЪрд╛рд▓рд╡рддреЛ.

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

рдкреНрд░рддреНрдпреЗрдХ desk рд╡рд░ рдкрд╛рд╣реБрдгреНрдпрд╛рдВрдЪреА рдпрд╛рджреА ЁЯзН рдШреЗрдКрди рдПрдХ рдореЙрдирд┐рдЯрд░ рдмрд╕рд▓реЗрд▓реА рдЕрд╕рддреЗ тАФ рд╣рд╛ security group. рдпрд╛рджреАрдд рдлрдХреНрдд рдХреЛрдг рдЖрдд рдпреЗрдК рд╢рдХрддреЗ рддреЗрдЪ рд▓рд┐рд╣рд┐рд▓реЗрд▓реЗ рдЕрд╕рддреЗ: "app wing рдордзрд▓реНрдпрд╛ рд╢рд┐рдХреНрд╖рд┐рдХрд╛, рдЦреЛрд▓реА 5432 рд╕рд╛рдареА". рддрд┐рдЪреА рд╕реНрдорд░рдгрд╢рдХреНрддреА рдкрд░рд┐рдкреВрд░реНрдг рдЖрд╣реЗ: рддрд┐рдиреЗ рдПрдЦрд╛рджреНрдпрд╛рд▓рд╛ рдЖрдд рд╕реЛрдбрд▓реЗ, рддрд░ рддреНрдпрд╛рдЪреЗ рдЙрддреНрддрд░ рдХреЛрдгрддрд╛рд╣реА рдкреНрд░рд╢реНрди рди рд╡рд┐рдЪрд╛рд░рддрд╛ рдкрд░рдд рдмрд╛рд╣реЗрд░ рдЬрд╛рдК рд╢рдХрддреЗ. рддреА рдХрдзреАрдЪ "рдЪрд╛рд▓рддреЗ рд╡реНрд╣рд╛" рдЕрд╕реЗ рдУрд░рдбрдд рдирд╛рд╣реА тАФ рдЕрдиреЛрд│рдЦреА рд╡реНрдпрдХреНрддреАрдХрдбреЗ рдлрдХреНрдд рджреБрд░реНрд▓рдХреНрд╖ рдХреЗрд▓реЗ рдЬрд╛рддреЗ.

рдкреНрд░рддреНрдпреЗрдХ wing рд▓рд╛ рдХреНрд░рдорд╛рдВрдХрд┐рдд рдирд┐рдпрдорд╛рдВрдЪрд╛ рдлрд▓рдХ рдЕрд╕рд▓реЗрд▓реЗ рдПрдХ рджрд╛рд░ ЁЯЪж рдЕрд╕рддреЗ тАФ рд╣рд╛ network ACL. рддреЗ рдЖрдд рдпреЗрдгрд╛рд▒реНрдпрд╛рдВрдирд╛ рдЖрдгрд┐ рдмрд╛рд╣реЗрд░ рдЬрд╛рдгрд╛рд▒реНрдпрд╛рдВрдирд╛ рддрдкрд╛рд╕рддреЗ, рдирд┐рдпрдо рд╕рд░реНрд╡рд╛рдд рд▓рд╣рд╛рди рдирдВрдмрд░рдкрд╛рд╕реВрди рд╡рд░ рд╡рд╛рдЪрддреЗ, рдЖрдгрд┐ рдЬреБрд│рдгрд╛рд░рд╛ рдкрд╣рд┐рд▓рд╛ рдирд┐рдпрдо рдирд┐рд░реНрдгрдп рджреЗрддреЛ тАФ allow рдХрд┐рдВрд╡рд╛ deny. рддреНрдпрд╛рд▓рд╛ рд╕реНрдорд░рдгрд╢рдХреНрддреА рдЕрдЬрд┐рдмрд╛рдд рдирд╛рд╣реА. рдЬреЗрд╡реНрд╣рд╛ рдЙрддреНрддрд░ рдмрд╛рд╣реЗрд░ рдЬрд╛рддреЗ, рддреЗрд╡реНрд╣рд╛ рджрд╛рд░ рддреЗ рдЕрдиреЛрд│рдЦреА рдЕрд╕рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ рддрдкрд╛рд╕рддреЗ. рдЙрддреНрддрд░рд╛рдВрдирд╛ рдмрд╛рд╣реЗрд░ рд╕реЛрдбрдгрд╛рд░рд╛ рдПрдХрд╣реА рдирд┐рдпрдо рдирд╕реЗрд▓, рддрд░ рдЙрддреНрддрд░ рдЕрдбрд╡рд▓реЗ рдЬрд╛рддреЗ, рдЖрдгрд┐ рдкрд╛рд╣реБрдгрд╛ рдХрд╛рдпрдордЪрд╛ рд╡рд╛рдЯ рдкрд╛рд╣рдд рд░рд╛рд╣рддреЛ.

рдПрдХрд╛ wing рдордзреВрди рджреБрд╕рд▒реНрдпрд╛ wing рдХрдбреЗ рдЬрд╛рдгрд╛рд░рд╛ packet рдЕрд╕рд╛ рдЬрд╛рддреЛ: рд╕реНрд╡рддрдГрдЪреА рдкрд╛рд╣реБрдгреНрдпрд╛рдВрдЪреА рдпрд╛рджреА (рдмрд╛рд╣реЗрд░) тЖТ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ wing рдЪреЗ рджрд╛рд░ (рдмрд╛рд╣реЗрд░) тЖТ corridor рдЪреА рдкрд╛рдЯреА тЖТ рджреБрд╕рд▒реНрдпрд╛ wing рдЪреЗ рджрд╛рд░ (рдЖрдд) тЖТ рджреБрд╕рд▒реНрдпрд╛ desk рдЪреА рдкрд╛рд╣реБрдгреНрдпрд╛рдВрдЪреА рдпрд╛рджреА (рдЖрдд).

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

flowchart LR
    web["ЁЯЦея╕П web 10.20.0.10"]
    s1["ЁЯзН sg-web out тЬУ"]
    n1["ЁЯЪж acl-open rule 100 ALLOW"]
    rt["ЁЯкз rt-public: 10.20.0.0/16 тЖТ local"]
    n2["ЁЯЪж acl-data rule 110 ALLOW<br/>(1024тАУ65535 from anywhere)"]
    s2["тЫФ sg-db in<br/>only 10.20.48.0/20 on the list"]
    web -->|"1"| s1 -->|"2"| n1 -->|"3"| rt -->|"4"| n2 -->|"5"| s2

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рддреЗ рдЙрд▓рдЯ рдкрджреНрдзрддреАрдиреЗ рдЪреБрдХрддрд╛рдд. Security groups рд╣реЗ рдореБрдЦреНрдп рд╕рд╛рдзрди тАФ рдиреЗрдордХреЗ, stateful, рд╕рдордЬрд╛рдпрд▓рд╛ рд╕реЛрдкреЗ тАФ рдЖрдгрд┐ рдЬрд╛рд╕реНрддрдЪ рдЙрдШрдбреЗ рдХрд░рд╛рдпрд▓рд╛рд╣реА рд╕реЛрдкреЗ. NACLs рдмреЛрдердЯ, рдкреВрд░реНрдг subnet рд╕рд╛рдареА, рдЖрдгрд┐ рдЪреБрдХрд╛рдпрд▓рд╛ рд╕реЛрдкреЗ: рдЙрддреНрддрд░рд╛рд╕рд╛рдареАрдЪрд╛ рдирд┐рдпрдо рдирд╕рд▓рд╛ рдХреА connections рдХреБрдареЗрд╣реА error рди рджреЗрддрд╛ рд▓рдЯрдХреВрди рд░рд╛рд╣рддрд╛рдд. рдмрд╣реБрддреЗрдХ teams рд╕рдЧрд│реНрдпрд╛рд╕рд╛рдареА SGs рд╡рд╛рдкрд░рддрд╛рдд рдЖрдгрд┐ NACLs рдлрдХреНрдд рдХрд╛рд╣реА subnet-wide denies рд╕рд╛рдареА.

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

SG.allows(rules, proto, port, ip) рдХреЛрдгрддрд╛рд╣реА рдирд┐рдпрдо рдЬреБрд│рд▓рд╛ рддрд░ true рдкрд░рдд рдХрд░рддреЛ тАФ deny рдирд╛рд╣реАрдЪ. NACL.__init__ рдирд┐рдпрдо рдирдВрдмрд░рдкреНрд░рдорд╛рдгреЗ рд▓рд╛рд╡рддреЛ; NACL.check() рдкрд╣рд┐рд▓рд╛ рдЬреБрд│рдгрд╛рд░рд╛ (allowed, "acl-тАж rule N ALLOW|DENY") рдореНрд╣рдгреВрди, рдХрд┐рдВрд╡рд╛ "final * DENY" рдкрд░рдд рдХрд░рддреЛ. Campus.walk() source SG рдЖрдгрд┐ NACL outbound, route, рдЖрдгрд┐ рдордЧ destination NACL рдЖрдгрд┐ SG inbound рддрдкрд╛рд╕рддреЛ. рддреЛ request рддрдкрд╛рд╕рддреЛ, рдЙрддреНрддрд░ рдирд╛рд╣реА тАФ рдЦрд╛рд▓рдЪрд╛ snippet рдЙрддреНрддрд░ рд╣рд╛рддрд╛рдиреЗ рддрдкрд╛рд╕рддреЛ.

Demo рдордзрд▓рд╛ acl-data рдиреАрдЯ рдкрд╛рд╣рд╛: рдирд┐рдпрдо 100 app wing рдордзреВрди 5432 рдЖрдд рдпреЗрдК рджреЗрддреЛ, рдЖрдгрд┐ рдирд┐рдпрдо 110 рдХреБрдареВрдирд╣реА 1024тАУ65535 рдЖрдд рдпреЗрдК рджреЗрддреЛ (рдЙрддреНрддрд░рд╛рдВрд╕рд╛рдареА рдареЗрд╡рд▓реЗрд▓рд╛). Port 5432 рд╣рд╛ 1024тАУ65535 рдЪреНрдпрд╛ рдЖрдд рдЖрд╣реЗ, рдореНрд╣рдгреВрди рдирд┐рдпрдо 110 web рдЪреА request рд╕реБрджреНрдзрд╛ рджрд╛рд░рд╛рддреВрди рд╕реЛрдбрддреЛ тАФ рдлрдХреНрдд рдкрд╛рд╣реБрдгреНрдпрд╛рдВрдЪреА рдпрд╛рджреАрдЪ рддреА рдерд╛рдВрдмрд╡рддреЗ. рдЙрддреНрддрд░реЗ allow рдХрд░рд╛рд╡реА рд▓рд╛рдЧрдгрд╛рд░реЗ stateless рджрд╛рд░ рдЕрдиреЗрдХрджрд╛ рддреБрдореНрд╣рд╛рд▓рд╛ рд╡рд╛рдЯрддреЗ рддреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рдЙрдШрдбреЗ рдЕрд╕рддреЗ.

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

python3 vpc/demo.py guards
python3 - <<'EOF'
import sys; sys.path.insert(0, "vpc"); from network import SG, NACL
from demo import campus
c = campus()
# 1) put the public wing on db's guest list
c.sgs["sg-db"] = SG("sg-db", [("tcp", (5432, 5432), "10.20.48.0/20"), ("tcp", (5432, 5432), "10.20.0.0/20")])
print("web тЖТ db, SG widened:", c.walk("web", "10.20.96.15", "tcp", 5432)[1][-2:])
# 2) a data-wing door with a numbered DENY for the public wing, before the allows
c.nacls["acl-data"] = NACL("acl-data",
    [(90, "deny", "all", (0, 65535), "10.20.0.0/20"), (100, "allow", "tcp", (5432, 5432), "10.20.48.0/20"), (110, "allow", "tcp", (1024, 65535), "0.0.0.0/0")],
    [(100, "allow", "tcp", (1024, 65535), "10.20.0.0/16")])
print("web тЖТ db, NACL rule 90:", c.walk("web", "10.20.96.15", "tcp", 5432)[1][-1])
print("app тЖТ db still:        ", c.walk("app", "10.20.96.15", "tcp", 5432)[0])
# 3) the reply: db answers app on its ephemeral port тАФ the data wing's OUTBOUND rules decide
acl = c.nacls["acl-data"]
print("reply out to 51514:", acl.check(acl.outbound, "tcp", 51514, "10.20.48.25"))
no_reply = NACL("acl-data", acl.inbound, [])
print("no outbound rules: ", no_reply.check(no_reply.outbound, "tcp", 51514, "10.20.48.25"))
EOF

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

guards web тЖТ 10.20.48.25:8080 тЬЕ рдЖрдгрд┐ app тЖТ 10.20.96.15:5432 тЬЕ рдЫрд╛рдкрддреЛ, рдЖрдгрд┐ рдордЧ:

   web тЖТ 10.20.96.15:5432  ЁЯЪл
        ЁЯзН sg-web out тЬУ
        ЁЯЪж acl-open rule 100 ALLOW
        ЁЯкз rt-public: 10.20.0.0/16 тЖТ local
        ЁЯЪж acl-data rule 110 ALLOW (destination subnet, in)
        тЫФ sg-db in тАФ not on the guest list

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

web тЖТ db, SG widened: ['ЁЯЪж acl-data rule 110 ALLOW (destination subnet, in)', 'ЁЯзН sg-db in тЬУ']
web тЖТ db, NACL rule 90: тЫФ acl-data rule 90 DENY (destination subnet, in)
app тЖТ db still:         True
reply out to 51514: (True, 'acl-data rule 100 ALLOW')
no outbound rules:  (False, 'acl-data final * DENY')

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

рддреБрдореНрд╣реА рдкреНрд░рддреНрдпреЗрдХ packet рдЪреЗ рднрд╛рдХреАрдд рдХреЗрд▓реЗ: рдкрд╛рд╣реБрдгреНрдпрд╛рдВрдЪреА рдпрд╛рджреА рдкреНрд░рддреНрдпреЗрдХ desk рд╕рд╛рдареА рдирд┐рд░реНрдгрдп рджреЗрддреЗ рдЖрдгрд┐ рдЙрддреНрддрд░реЗ рд▓рдХреНрд╖рд╛рдд рдареЗрд╡рддреЗ; wing рдЪреЗ рджрд╛рд░ рд╕рд░реНрд╡рд╛рдд рд▓рд╣рд╛рди рдирдВрдмрд░рдкрд╛рд╕реВрди рд╡рд░ рд╡рд╛рдЪрддреЗ, deny рдХрд░реВ рд╢рдХрддреЗ, рдЖрдгрд┐ рд╡рд┐рд╕рд░рддреЗ тАФ рдореНрд╣рдгреВрди outbound рдирд┐рдпрдо рдирд╕рд▓реЗрд▓реЗ рдЙрддреНрддрд░ рд╢реЗрд╡рдЯрдЪреНрдпрд╛ * рд╡рд░ рдорд░рддреЗ.

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

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

рдиреЗрд╣рдореАрдЪреА рд░рдЪрдирд╛: load balancer рдЪрд╛ SG internet рд╡рд░реВрди 443 рдШреЗрддреЛ, app SG рдлрдХреНрдд load balancer рдЪреНрдпрд╛ SG рдХрдбреВрди traffic рдШреЗрддреЛ, database SG рдлрдХреНрдд app SG рдХрдбреВрди 5432 рдШреЗрддреЛ. Subnet-wide block рд▓рд╛рдЧрд▓реНрдпрд╛рд╢рд┐рд╡рд╛рдп NACLs default рд╡рд░рдЪ рд░рд╛рд╣рддрд╛рдд.

рдЦрд▒реНрдпрд╛ account рд╡рд░ (AWS CLI рдЖрдгрд┐ credentials рд▓рд╛рдЧрддрд╛рдд; IDs рдЙрджрд╛рд╣рд░рдгрд╛рджрд╛рдЦрд▓ рдЖрд╣реЗрдд):

aws ec2 authorize-security-group-ingress --group-id sg-0db0000000000000a \
    --protocol tcp --port 5432 --source-group sg-0app000000000000a
aws ec2 create-network-acl-entry --network-acl-id acl-0123456789abcdef0 --ingress --rule-number 90 \
    --protocol -1 --cidr-block 10.20.0.0/20 --rule-action deny
aws ec2 create-network-acl-entry --network-acl-id acl-0123456789abcdef0 --egress --rule-number 100 \
    --protocol tcp --port-range From=1024,To=65535 --cidr-block 10.20.0.0/16 --rule-action allow

рд╣рд╛рдЪ SG рдирд┐рдпрдо Terraform рдордзреНрдпреЗ:

resource "aws_vpc_security_group_ingress_rule" "db_from_app" {
  security_group_id            = aws_security_group.db.id
  referenced_security_group_id = aws_security_group.app.id
  ip_protocol                  = "tcp"
  from_port                    = 5432
  to_port                      = 5432
}

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

рднрд╛рдЧ 1 рдкреВрд░реНрдг рдЭрд╛рд▓рд╛: рддреБрдореНрд╣реА campus рдмрд╛рдВрдзрд▓рд╛. рднрд╛рдЧ 2 рддреЛ рдЬреЛрдбрддреЛ тАФ рд╕рд░реНрд╡рд╛рдд рдЖрдзреА AWS рд╢реАрдЪ. Private wing рджрд░рд╡рд╛рдЬрд╛рддреВрди рди рдЬрд╛рддрд╛ S3 рдкрд░реНрдпрдВрдд рдХрд╕рд╛ рдкреЛрд╣реЛрдЪрддреЛ? VPC endpoints тАФ рдПрдХ рдЦрд╛рдЬрдЧреА рдмреЛрдЧрджрд╛.

git checkout lesson-07-endpoints

ЁЯзН Lesson 06 тАФ Security groups vs NACLs: the guest list and the wing door

ЁЯУН You are here: Lesson 06 of 12 ┬╖ Previous: lesson-05-igw-public ┬╖ Next: lesson-07-endpoints


ЁЯУж What's in this branch

Lessons 01тАУ05, plus the two guards every packet meets inside the campus: SG (the guest list at the desk тАФ stateful) and NACL (the wing door тАФ stateless, numbered) in vpc/network.py. guards() in vpc/demo.py walks three packets through both, step by step.

ЁЯзТ Explain like I'm 5

Every desk has a monitor with a guest list ЁЯзН тАФ the security group. The list only says who may come in: "teachers from the app wing, for room 5432". It has a perfect memory: if it let someone in, their answer may walk back out, no questions asked. It never shouts "go away" тАФ a stranger is simply ignored.

Every wing has a door ЁЯЪж with a numbered rule board тАФ the network ACL. It checks people walking in and people walking out, reads the rules from the lowest number up, and the first rule that matches decides тАФ allow or deny. It has no memory. When an answer walks out, the door checks it like a stranger. If no rule lets answers out, the answer is stopped, and the visitor waits forever.

A packet from one wing to another passes: its own guest list out тЖТ its own wing door out тЖТ the corridor sign тЖТ the other wing's door in тЖТ the other desk's guest list in.

ЁЯЧ║я╕П Diagram

flowchart LR
    web["ЁЯЦея╕П web 10.20.0.10"]
    s1["ЁЯзН sg-web out тЬУ"]
    n1["ЁЯЪж acl-open rule 100 ALLOW"]
    rt["ЁЯкз rt-public: 10.20.0.0/16 тЖТ local"]
    n2["ЁЯЪж acl-data rule 110 ALLOW<br/>(1024тАУ65535 from anywhere)"]
    s2["тЫФ sg-db in<br/>only 10.20.48.0/20 on the list"]
    web -->|"1"| s1 -->|"2"| n1 -->|"3"| rt -->|"4"| n2 -->|"5"| s2

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

тЭУ What

ЁЯдФ Why

Because they fail in opposite ways. Security groups are the main tool тАФ precise, stateful, easy to reason about тАФ and easy to open too wide. NACLs are blunt, subnet-wide, and easy to get wrong: a missing reply rule makes connections hang with no error anywhere. Most teams use SGs for everything and NACLs only for a few subnet-wide denies.

ЁЯФз How (in this repo)

SG.allows(rules, proto, port, ip) returns true if any rule matches тАФ there is no deny. NACL.__init__ sorts the rules by number; NACL.check() returns the first match as (allowed, "acl-тАж rule N ALLOW|DENY"), or "final * DENY". Campus.walk() checks the source SG and NACL outbound, the route, then the destination NACL and SG inbound. It checks the request, not the reply тАФ the snippet below checks the reply by hand.

Look closely at the demo's acl-data: rule 100 lets 5432 in from the app wing, and rule 110 lets 1024тАУ65535 in from anywhere (meant for replies). Port 5432 is inside 1024тАУ65535, so rule 110 also lets web's request through the door тАФ only the guest list stops it. A stateless door that must allow replies is often wider than you think.

ЁЯзк Try it

python3 vpc/demo.py guards
python3 - <<'EOF'
import sys; sys.path.insert(0, "vpc"); from network import SG, NACL
from demo import campus
c = campus()
# 1) put the public wing on db's guest list
c.sgs["sg-db"] = SG("sg-db", [("tcp", (5432, 5432), "10.20.48.0/20"), ("tcp", (5432, 5432), "10.20.0.0/20")])
print("web тЖТ db, SG widened:", c.walk("web", "10.20.96.15", "tcp", 5432)[1][-2:])
# 2) a data-wing door with a numbered DENY for the public wing, before the allows
c.nacls["acl-data"] = NACL("acl-data",
    [(90, "deny", "all", (0, 65535), "10.20.0.0/20"), (100, "allow", "tcp", (5432, 5432), "10.20.48.0/20"), (110, "allow", "tcp", (1024, 65535), "0.0.0.0/0")],
    [(100, "allow", "tcp", (1024, 65535), "10.20.0.0/16")])
print("web тЖТ db, NACL rule 90:", c.walk("web", "10.20.96.15", "tcp", 5432)[1][-1])
print("app тЖТ db still:        ", c.walk("app", "10.20.96.15", "tcp", 5432)[0])
# 3) the reply: db answers app on its ephemeral port тАФ the data wing's OUTBOUND rules decide
acl = c.nacls["acl-data"]
print("reply out to 51514:", acl.check(acl.outbound, "tcp", 51514, "10.20.48.25"))
no_reply = NACL("acl-data", acl.inbound, [])
print("no outbound rules: ", no_reply.check(no_reply.outbound, "tcp", 51514, "10.20.48.25"))
EOF

тЬЕ Verify тАФ what you should see

guards prints web тЖТ 10.20.48.25:8080 тЬЕ and app тЖТ 10.20.96.15:5432 тЬЕ, then:

   web тЖТ 10.20.96.15:5432  ЁЯЪл
        ЁЯзН sg-web out тЬУ
        ЁЯЪж acl-open rule 100 ALLOW
        ЁЯкз rt-public: 10.20.0.0/16 тЖТ local
        ЁЯЪж acl-data rule 110 ALLOW (destination subnet, in)
        тЫФ sg-db in тАФ not on the guest list

Your snippet prints:

web тЖТ db, SG widened: ['ЁЯЪж acl-data rule 110 ALLOW (destination subnet, in)', 'ЁЯзН sg-db in тЬУ']
web тЖТ db, NACL rule 90: тЫФ acl-data rule 90 DENY (destination subnet, in)
app тЖТ db still:         True
reply out to 51514: (True, 'acl-data rule 100 ALLOW')
no outbound rules:  (False, 'acl-data final * DENY')

ЁЯПБ What you just proved

You predicted every packet: the guest list decides per desk and remembers replies; the wing door reads numbers from the lowest up, can deny, and forgets тАФ so a reply with no outbound rule dies at the final *.

тЪая╕П Common mistakes

ЁЯПн In production

The standard shape: the load balancer's SG takes 443 from the internet, the app SG takes traffic only from the load balancer's SG, the database SG takes 5432 only from the app SG. NACLs stay at the default unless you need a subnet-wide block.

On a real account (needs the AWS CLI and credentials; IDs are examples):

aws ec2 authorize-security-group-ingress --group-id sg-0db0000000000000a \
    --protocol tcp --port 5432 --source-group sg-0app000000000000a
aws ec2 create-network-acl-entry --network-acl-id acl-0123456789abcdef0 --ingress --rule-number 90 \
    --protocol -1 --cidr-block 10.20.0.0/20 --rule-action deny
aws ec2 create-network-acl-entry --network-acl-id acl-0123456789abcdef0 --egress --rule-number 100 \
    --protocol tcp --port-range From=1024,To=65535 --cidr-block 10.20.0.0/16 --rule-action allow

The same SG rule in Terraform:

resource "aws_vpc_security_group_ingress_rule" "db_from_app" {
  security_group_id            = aws_security_group.db.id
  referenced_security_group_id = aws_security_group.app.id
  ip_protocol                  = "tcp"
  from_port                    = 5432
  to_port                      = 5432
}

тПня╕П Next

Part 1 is done: you built a campus. Part 2 connects it тАФ first to AWS itself. How does a private wing reach S3 without going through the gate? VPC endpoints тАФ a private tunnel.

git checkout lesson-07-endpoints
тЖР Previousigw publicNext тЖТendpoints

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