Bucket, którego nigdy nie należało otwierać
Poproszono nas o upublicznienie bucketu S3, żeby roboty mogły pobrać paczkę certyfikatów głównych tożsamości. Prośba przyszła z już przygotowaną polityką — klasyczną polityką bucketu z publicznym odczytem. Wyglądało na dziesięć minut pracy. Nie było.
Haczyk: „upublicznij to”
Właściciel obciążenia napisał: paczka musi być dostępna dla robotów, a na produkcji zwykła domena brzegowa jej nie dostarcza. Proponowana poprawka była o jedną linię dalej: dołączyć politykę bucketu dającą s3:GetObject dla Principal: "*", żeby każdy mógł pobrać plik bezpośrednio z S3.
Był dobry powód, dla którego to wydawało się słuszne: paczka jest z natury publiczna — to zestaw publicznych certyfikatów głównych CA, nic tajnego. Liczy się integralność, a nie tajność. Więc „publiczny” brzmiało nieszkodliwie.
Ale „publiczny bucket” i „publiczna treść” to różne rzeczy, a ta poprawka przesunęłaby niewłaściwą dźwignię.
Pierwszy zwrot: brzeg, a nie bucket
Prośba tak naprawdę dotyczyła dostępnego publicznego URL-a. To zadanie brzegu — CDN przed bucketem. A z brzegiem dostajesz publiczną dostępność bez upubliczniania bucketu w ogóle.
Właściwym mechanizmem jest Origin Access Control (OAC): CDN uwierzytelnia się w S3 własnymi sekretami, ograniczonymi do jednej dystrybucji, a bucket zostaje w pełni zamknięty — wszystkie cztery ustawienia Block-Public-Access nadal włączone.
Zamiast polityki z Principal: "*" polityka bucketu mówiła więc coś odwrotnego:
Principal = { Service = "cloudfront.amazonaws.com" } # not "*"
Action = "s3:GetObject"
Resource = <the bundle object only>
Condition = { StringEquals = { "AWS:SourceArn" = <this one distribution> } }
Publiczne dla internetu, niewidoczne dla wszystkiego poza jednym uwierzytelnionym brzegiem. Bucket nigdy nie został „otwarty”.
Zwrot, który kosztował ponowne wdrożenie: domyślna flaga
Pierwsze wdrożenie poszło. Potem prawdziwy test — pobranie paczki przez nowy endpoint:
HTTP/2 403 x-cache: Error from cloudfront server: AmazonS3
403 z x-cache: Error from cloudfront — błąd przyszedł z S3, nie z CloudFronta. Brzeg docierał do originu i dostawał odmowę. Skonfigurowaliśmy OAC i politykę... a nie zadziałało.
Króliczą norą był moduł Terraforma. Używaliśmy społecznościowego modułu CloudFront, a w v4 tego modułu create_origin_access_control domyślnie ma false. Nikt celowo nie przełącza flagi na „bez OAC”, ale wartość domyślna zrobiła dokładnie to: dystrybucja powstała z nieuwierzytelnionym originem S3 — bez żadnego OAC. Każde pobranie trafiało w prywatny bucket bez poświadczeń i było odprawiane.
Sygnał był wprost w wyjściu API:
OriginAccessControlId: "" # empty — the origin was naked
Naprawiła to jedna linia — jawne włączenie create_origin_access_control = true — i odpowiedź zmieniła się na 200.
Pułapka regionu: certyfikaty żyją tylko w jednym miejscu
Następnie chcieliśmy przyjaznej nazwy hosta zamiast surowej domeny CloudFronta. To wymaga certyfikatu ACM, który właściciel już miał — wildcard obejmujący każdą subdomenę.
Oto część, która łapie każdego: CloudFront przyjmuje tylko certyfikaty z ACM w us-east-1. Nasz certyfikat wildcard istniał w innym regionie (gdzie obsługuje load balancery). Dla CloudFronta równie dobrze mógłby nie istnieć. Regionalny certyfikat w „złym” regionie jest niewidoczny dla dystrybucji.
Nie walczyliśmy z tym — wystawiliśmy osobny certyfikat dla tej konkretnej nazwy hosta w us-east-1 i zostawiliśmy regionalny wildcard w spokoju. Dwa certyfikaty, różne regiony, różne zadania, bez konfliktu.
Fałszywy alarm: „DNS się nie rozwiązuje”
Po tym wszystkim ostatnia kontrola: rozwiązać nazwę hosta.
curl: (6) Could not resolve host: ...
Kolejna ślepa uliczka? Nie — lokalna maszyna była za VPN, a jej DNS serwował nieaktualny cache „to nie istnieje” sprzed utworzenia rekordu. Publiczne resolwery odpowiadały natychmiast, a endpoint serwował 200 każdemu, kto nie siedział za tym jednym nieaktualnym cache. Prawdziwie wyglądająca awaria z przyziemną przyczyną:
public DNS (any) -> CloudFront IPs -> 200 ✓ local VPN resolver -> "No answer" -> curl error ✗ (just caching)
Rozwiązanie
Paczka jest dostarczana przez HTTPS z porządnego brzegu. Bucket jest prywatny — nigdzie nie ma Principal:"*". Dystrybucja używa osobnego certyfikatu z us-east-1 przy TLSv1.2_2021. Zarówno domyślna domena CloudFronta, jak i przyjazna nazwa hosta zwracają 200 z pełną zawartością paczki.
Wskazówki dla czytelnika
- „Publiczna treść” i „publiczny bucket” to różne rzeczy. Jeśli potrzebujesz publicznego URL-a, postaw brzeg (OAC) przed prywatnym bucketem — nigdy nie otwieraj bucketu dla
*. - Kiedy
403wraca jakox-cache: Error from cloudfrontiserver: AmazonS3, odmowa jest na odcinku do originu, a nie po stronie widza. Sprawdź, czy origin faktycznie jest uwierzytelniony (czy OAC jest dołączone), a nie tylko, czy zamierzałeś je dołączyć. - Czytaj wartości domyślne modułu, którego używasz ponownie. Społecznościowy moduł CloudFront v4 po cichu dostarcza nieuwierzytelniony origin, jeśli nie włączysz OAC. „Nieszkodliwa wartość domyślna”, której nie zauważyłeś, to pułapka.
- CloudFront jest wybredny co do regionu certyfikatów. Twój wildcard w innym regionie jest dla niego bezwartościowy; certyfikat musi być w
us-east-1. - Lokalny błąd rozwiązywania nazwy to nie awaria. Sprawdź publiczny resolwer, zanim obwinisz DNS. Starsze odpowiedzi „brak odpowiedzi” są cache'owane przez routery i VPN i okłamują cię dłużej, niż się spodziewasz.