Si votre appelant n’a pas s3:ListBucket sur le bucket, S3 retourne 403 Access Denied pour un objet manquant au lieu de 404 Not Found. C’est délibéré : un 404 confirmerait à un appelant non autorisé quelles clés existent et lesquelles n’existent pas, alors S3 refuse de faire la distinction. La conséquence pratique : la moitié des « erreurs de permissions » que les équipes déboguent sont en réalité des fautes de frappe dans la clé, des objets supprimés ou des marqueurs de suppression — et la façon la plus rapide de trancher est de poser la question avec une identité qui peut lister.
Ce qui se passe
La logique d’erreur de S3 pour un GET sur une clé inexistante dépend de la permission de listage de l’appelant. Avec s3:ListBucket sur l’ARN du bucket, vous recevez 404 NoSuchKey — S3 vous fait assez confiance pour révéler l’existence, puisque vous auriez pu la découvrir en listant de toute façon. Sans elle, vous recevez 403 AccessDenied, identique à un vrai refus de permission. Le design empêche l’énumération de l’espace de clés : un attaquant qui sonde backup.sql, .env, secrets.json contre un bucket privé n’apprend rien de 403 uniformes.
Trois autres cas brouillent encore la ligne. Un marqueur de suppression sur un objet versionné fait retourner 404 au GET même si les données existent encore dans des versions antérieures. Une mauvaise région ou un mauvais point de terminaison peut produire des redirections et des erreurs que les clients remontent de façon incohérente. Et Block Public Access transforme ce qu’un appelant anonyme s’attendrait à voir comme 404 en 403 aussi, parce que l’appelant anonyme n’a par définition aucune permission de listage.
La confusion inverse arrive aussi : des équipes voient un 404, concluent « objet disparu », et manquent le fait qu’elles interrogent le mauvais bucket, le bucket du même patron de nom dans la mauvaise région, ou une clé avec une différence invisible — encodage URL, préfixe dédoublé comme uploads/uploads/file.pdf, espace final, ou casse (Report.PDF et report.pdf sont des objets différents ; les clés sont sensibles à la casse).
Quoi vérifier en premier
- Votre identité a-t-elle
s3:ListBucketsurarn:aws:s3:::le-bucket(l’ARN du bucket, pas/*)? Sinon, chaque 403 est ambigu par design. - La clé est-elle exactement la bonne? Copiez-la d’un résultat
list-objects-v2, jamais de mémoire ni de journaux qui ont pu l’encoder ou la décoder. - Le bucket est-il versionné? Si oui, un marqueur de suppression répond peut-être à votre GET.
- Êtes-vous dans le bon compte et la bonne région? Les buckets aux noms semblables entre environnements (
myapp-prod-assetsvsmyapp-staging-assets) causent une part surprenante de ces cas.
Comment vérifier
Levez l’ambiguïté avec une identité qui a à la fois s3:GetObject et s3:ListBucket — une session admin convient pour le diagnostic :
# La clé existe-t-elle? head-object est la sonde la moins coûteuse
aws s3api head-object --bucket my-bucket --key uploads/report.pdf
# Si ça donne 404, cherchez les quasi-correspondances dans le préfixe
aws s3api list-objects-v2 --bucket my-bucket \
--prefix uploads/rep --query 'Contents[].Key'
Sur un bucket versionné, cherchez les marqueurs de suppression :
aws s3api list-object-versions --bucket my-bucket \
--prefix uploads/report.pdf \
--query '{Versions:Versions[].[Key,VersionId,IsLatest],DeleteMarkers:DeleteMarkers[].[Key,VersionId,IsLatest]}'
Un marqueur de suppression avec IsLatest: true signifie que l’objet a été « supprimé » mais reste récupérable. Confirmez quelle identité votre application défaillante utilise vraiment, puis testez sous cette identité — l’assomption de rôle rend ça rapide :
aws sts get-caller-identity
aws s3api head-object --bucket my-bucket --key uploads/report.pdf --profile app-role
head-object retourne 404 ou 403 selon les mêmes règles que GET, et les événements de données CloudTrail (si activés) enregistrent le vrai errorCode côté serveur. Vérification console : S3 > Buckets > my-bucket — cherchez le préfixe, et activez Show versions sur un bucket versionné.
Le correctif
- Diagnostiquez d’abord avec une identité qui peut lister. Un seul appel
head-objectsous un rôle admin ou de diagnostic fait tomber l’ambiguïté :404veut dire que l’objet n’y est pas ;403sous une identité pleinement permissionnée veut dire un vrai refus de politique ou de KMS, alors basculez vers l’ordre de débogage du 403. - Si c’est un objet manquant, trouvez pourquoi. Vérifiez
list-object-versionspour les marqueurs de suppression, les règles de cycle de vie qui l’auraient fait expirer (Management > Lifecycle rules), et les journaux de l’application écrivaine — très souvent le téléversement a échoué silencieusement et rien n’a jamais existé. - Si c’est un écart de clé, corrigez l’écrivain ou le lecteur, pas les données. Normalisez la construction des clés dans une fonction partagée ; la plupart des bogues de double préfixe et d’encodage viennent d’une logique de concaténation dupliquée dans deux bases de code.
- Accordez
s3:ListBucketaux rôles applicatifs qui ont légitimement besoin de vrais 404. Les applications qui branchent sur « introuvable » (caches, vérifications d’existence) se comportent bien mieux quand S3 peut réellement dire 404. Limitez avec une conditions3:prefixsi le bucket contient plusieurs locataires. - Si des appelants anonymes ou d’autres comptes ont besoin de 404 propres, réfléchissez-y à deux fois. Il faudrait leur accorder le droit de lister, ce qui révèle votre espace de clés. La meilleure réponse est habituellement de placer CloudFront ou une API devant S3, qui possède alors la sémantique d’erreur.
Quand c’est la mauvaise approche
N’accordez pas s3:ListBucket largement juste pour embellir les codes d’erreur — sur des buckets multilocataires, la permission de lister à travers les locataires est une fuite de données (les clés d’objets contiennent souvent des noms de clients, des numéros de facture, des adresses courriel). Si votre application a besoin d’une sémantique d’existence précise sur des données sensibles, mettez une base de données devant et traitez S3 comme du stockage de blobs, ou utilisez des conditions de listage étroitement limitées par préfixe.
Erreurs fréquentes
- Passer des heures dans IAM pour ce qu’un simple
list-objects-v2 --prefixaurait exposé comme une faute de frappe. - Tester avec vos propres identifiants admin, obtenir un 404 propre, et ne pas remarquer que le rôle de l’application voit 403 pour la même clé manquante — puis « corriger » des politiques qui n’ont jamais été brisées.
- Oublier que les erreurs
HeadObjectn’ont pas de corps XML — un 403 nu d’un HEAD vous en dit encore moins qu’un GET ; reproduisez avec GET quand vous avez besoin du détail du code d’erreur. - Supposer qu’un 404 prouve la suppression alors que le nom de bucket, la région ou le profil était mauvais depuis le début.
Prévention
- Donnez aux rôles applicatifs de production
s3:ListBucketavec une condition de préfixe quand la logique branche sur l’existence — délibéré, limité, documenté. - Activez les événements de données CloudTrail sur les buckets où vous enquêtez régulièrement sur des questions d’accès ; les codes d’erreur côté serveur mettent fin aux devinettes.
- Centralisez la construction des clés et journalisez la clé exacte à chaque appel S3 échoué — la chaîne complète, pas un résumé interpolé.
- Gardez la page de sémantique d’erreurs S3 de https://docs.aws.amazon.com/AmazonS3/ dans le runbook d’équipe pour que le comportement « le 403 cache le 404 » soit un savoir d’équipe, pas une surprise récurrente.
La règle est compacte : un 403 sans permission de listage est illisible — levez-le en redemandant avec une identité qui peut lister, puis suivez l’erreur que vous avez réellement. Si personne dans votre équipe ne possède vraiment l’environnement AWS de bout en bout, c’est la racine de la plupart de ces problèmes — Stack Harbor opère des comptes AWS au quotidien sous Opérations AWS, et vous pouvez réserver une revue AWS.