no idea how this works with CloudFlare, but an S3 bucket policy to do this checks the referer.
The code below has nothing to do with discourse, it is protecting some files in a special paid section of an old website I have.
{"Version":"2008-10-17",
"Id":"preventHotLinking",
"Statement":[
{"Sid":"1","Effect":"Allow","Principal":
{"AWS":"*"},"Action":"s3:GetObject",
"Resource":"arn:aws:s3:::downloads.ephotopros.com/*",
"Condition":{"StringLike":{"aws:Referer":
["http://dev.ephotopros.com/*","http://ephotopros.com.s3.amazonaws.com/*",
"http://ephotopros.com/*","http://s3.amazonaws.com/ephotopros.com/*",
"http://localhost:4000/*","http://prolabprints.com/*"]}}}]}
The files are served from the main domain, the downloads sub domain is just the bucket name on S3. So the referrer check needs to match where the files should be allowed to be served from.
Interesting that it looks like Referer is mis-spelled but that is working code.