Cloudflare

Domain, storage and SSL, all the way to AWS.

Buy and configure a domain, serve files from R2 with presigned URLs and a CDN, and keep every hop HTTPS up to your AWS load balancer.

Visão geralOverview

A Cloudflare entra em três papéis neste desenho: registrar do domínio e DNS, CDN e proxy na frente da sua aplicação, e storage de arquivos (R2). A AWS continua rodando a aplicação atrás de um ALB. O objetivo é que toda a viagem seja HTTPS.Cloudflare plays three roles in this layout: domain registrar and DNS, a CDN and proxy in front of your application, and file storage (R2). AWS keeps running the application behind an ALB. The goal is for the whole trip to be HTTPS.

  1. Navegador → Cloudflare: o navegador abre https://app.exemplo.com. A Cloudflare responde com o certificado de borda (Universal SSL).Browser → Cloudflare: the browser opens https://app.example.com. Cloudflare answers with its edge certificate (Universal SSL).
  2. Cloudflare → ALB: com o modo Full (strict), a Cloudflare abre uma conexão HTTPS com o ALB e valida o certificado de origem que você instalou nele.Cloudflare → ALB: with Full (strict) mode, Cloudflare opens an HTTPS connection to the ALB and validates the origin certificate you installed on it.
  3. Arquivos: o navegador baixa e envia mídia direto do R2, por URL pré-assinada (bucket privado) ou por um domínio público com CDN (bucket público).Files: the browser downloads and uploads media straight from R2, through a presigned URL (private bucket) or a public custom domain with CDN (public bucket).
As seções seguem a ordem de montagem: domínio, DNS, SSL, R2, URLs pré-assinadas, como o navegador renderiza a mídia, variáveis, e por fim o certificado no ALB.The sections follow the build order: domain, DNS, SSL, R2, presigned URLs, how the browser renders media, variables, and finally the certificate on the ALB.

1. Comprar um domínio1. Buy a domain

No painel da Cloudflare: Register domains. O caminho é:In the Cloudflare dashboard: Register domains. The path is:

  1. Pesquise o nome ou palavras-chave. Domínios que não aparecem nas sugestões não estão disponíveis.Search for a name or keywords. Domains missing from the suggestions are unavailable.
  2. Clique em Purchase; a Cloudflare faz uma última checagem de disponibilidade.Click Purchase; Cloudflare runs a final availability check.
  3. Escolha o prazo (a maioria das TLDs vai até dez anos). A renovação automática vem ligada e pode ser desligada depois.Pick the term (most TLDs go up to ten years). Auto-renew is on by default and can be turned off later.
  4. Preencha os contatos (registrante, administrativo, técnico e cobrança) e a forma de pagamento.Fill in the contacts (registrant, admin, technical and billing) and the payment method.
  5. Revise os contratos e clique em Complete purchase. O registro leva até cerca de 30 segundos.Review the agreements and click Complete purchase. Registration takes up to about 30 seconds.

Requisitos e pegadinhasRequirements and gotchas

  • Precisa de um e-mail verificado na conta e de uma forma de pagamento.You need a verified email on the account and a payment method.
  • Os contatos aceitam só caracteres ASCII, e devem ser completos e corretos: dados inexatos podem levar à suspensão ou cancelamento.Contacts accept ASCII characters only, and must be complete and accurate: inaccurate details can lead to suspension or cancellation.
  • Verifique o e-mail do registrante. Se a verificação ficar pendente ou expirar, a ICANN coloca o domínio em hold e os nameservers viram servidores de estacionamento até a verificação ser concluída.Verify the registrant email. If verification stays pending or expires, ICANN places a hold on the domain and the nameservers are replaced with parking nameservers until verification is completed.
  • Domínios registrados lá usam os nameservers da Cloudflare, e não é possível trocá-los por outro provedor de DNS enquanto estiver no Registrar.Domains registered there use Cloudflare nameservers, and you cannot switch to another DNS provider’s nameservers while using Registrar.
  • Nomes internacionalizados (IDN, com acentos) não são suportados. A Cloudflare recomenda ligar o DNSSEC.Internationalised domain names (IDNs, with accents) are not supported. Cloudflare recommends enabling DNSSEC.

2. Configurar o domínio e o DNS2. Configure the domain and DNS

Se você comprou pela Cloudflare, o domínio já usa os nameservers dela. Se comprou em outro registrador, adicione-o à Cloudflare e troque os nameservers lá.If you bought through Cloudflare, the domain already uses its nameservers. If you bought somewhere else, add it to Cloudflare and change the nameservers at your registrar.

Domínio vindo de outro registradorDomain from another registrar

  1. Em Domains → Onboard a domain, informe o domínio raiz (por exemplo exemplo.com; subdomínios não servem) e escolha um plano.In Domains → Onboard a domain, enter the apex domain (for example example.com; subdomains do not work) and pick a plan.
  2. Revise os registros DNS. O scan automático pode não achar todos: confira o registro da raiz, o www e os de e-mail (MX, SPF, DKIM, DMARC).Review the DNS records. The automatic scan may not find every one: check the apex record, www and the email ones (MX, SPF, DKIM, DMARC).
  3. A Cloudflare indica dois nameservers. No registrador, troque os atuais por esses dois.Cloudflare assigns two nameservers. At your registrar, replace the current ones with them.
  4. Aguarde a ativação (um domínio novo pode levar até 24 horas para ser verificado). Depois disso a Cloudflare passa a ser seu DNS e proxy reverso.Wait for activation (a newly added domain can take up to 24 hours to be verified). After that, Cloudflare is your DNS and reverse proxy.
Se o DNSSEC estiver ligado no registrador, desligue-o antes de trocar os nameservers; senão o domínio pode ficar inacessível. Reative-o depois, pela Cloudflare.If DNSSEC is enabled at the registrar, turn it off before changing nameservers; otherwise the domain can become unreachable. Re-enable it afterwards, through Cloudflare.

Proxy ligado ou desligadoProxied or DNS only

Cada registro A, AAAA ou CNAME tem um status de proxy:Each A, AAAA or CNAME record has a proxy status:

StatusStatusO que aconteceWhat happens
Proxied (nuvem laranja)Proxied (orange cloud)O tráfego HTTP/HTTPS passa pela Cloudflare: CDN, cache, WAF, proteção DDoS. O DNS devolve IPs anycast da Cloudflare, e não o IP da sua origem.HTTP/HTTPS traffic goes through Cloudflare: CDN, cache, WAF, DDoS protection. DNS returns Cloudflare anycast IPs, not your origin IP.
DNS only (nuvem cinza)DNS only (grey cloud)O DNS devolve o endereço real e o tráfego não passa pela Cloudflare. Sua origem fica exposta. Use para registros que não são web, como e-mail e verificação de domínio.DNS returns the real address and traffic does not pass through Cloudflare. Your origin is exposed. Use it for non-web records, such as email and domain verification.

Registros deste desenhoRecords for this layout

TypeNameTargetProxyProxy
CNAMEappDNS do ALB (xxx.us-east-1.elb.amazonaws.com)the ALB DNS name (xxx.us-east-1.elb.amazonaws.com)ProxiedProxied
CNAMEmediacriado pelo R2 ao conectar o domínio personalizadocreated by R2 when you connect the custom domainProxiedProxied
CNAME_xxxx (validação do ACM)_xxxx (ACM validation)valor fornecido pelo ACM, se usar certificado público da AWSvalue given by ACM, if you use an AWS public certificateDNS onlyDNS only
Limitação do proxy: um registro com proxy tem TTL fixo em Auto (300 s), e se a origem não responder dentro do Proxy Read Timeout a Cloudflare devolve o erro 524.Proxy limitation: a proxied record has a fixed TTL of Auto (300 s), and if the origin does not respond within the Proxy Read Timeout Cloudflare returns a 524 error.

3. Certificado SSL e CDN3. SSL certificate and CDN

Há dois trechos de conexão, e cada um tem seu certificado:There are two connection legs, and each has its own certificate:

TrechoLegCertificadoCertificateQuem cuidaWho handles it
Visitante → CloudflareVisitor → CloudflareCertificado de borda (Universal SSL), gratuito e confiável para navegadoresEdge certificate (Universal SSL), free and browser-trustedA Cloudflare emite, renova e instala sozinha.Cloudflare issues, renews and installs it automatically.
Cloudflare → origem (ALB)Cloudflare → origin (ALB)Certificado de origem (Cloudflare Origin CA ou de uma CA pública)Origin certificate (Cloudflare Origin CA or from a public CA)Você instala no ALB (seção 9).You install it on the ALB (section 9).

Universal SSL (borda)Universal SSL (edge)

É um certificado DV gratuito, que cobre o domínio raiz e os subdomínios de primeiro nível (exemplo.com, app.exemplo.com). Só é emitido depois que o domínio está ativo na Cloudflare. Subdomínios de vários níveis (a.b.exemplo.com) não são cobertos em setup completo; exigem Total TLS ou certificados avançados.It is a free DV certificate covering the apex domain and first-level subdomains (example.com, app.example.com). It is only issued once the domain is active on Cloudflare. Multi-level subdomains (a.b.example.com) are not covered on a full setup; they need Total TLS or advanced certificates.

Modos de criptografiaEncryption modes

ModoModeVisitante → CloudflareVisitor → CloudflareCloudflare → origemCloudflare → originValida o certificado da origem?Validates origin certificate?
OffHTTPHTTPNãoNo
FlexibleHTTPSHTTPNãoNo
FullHTTPSAcompanha o protocolo do visitanteMatches the visitor’s protocolNão (aceita autoassinado)No (accepts self-signed)
Full (strict)HTTPSHTTPSSimYes
Strict (SSL-Only Origin Pull)HTTP ou HTTPSHTTP or HTTPSSempre HTTPSAlways HTTPSSimYes
Use Full (strict). É o único dos modos comuns que cifra o trecho até a origem e confere o certificado. O Full (strict) aceita certificado de uma CA pública (como o Let’s Encrypt ou um certificado público do ACM) ou da Cloudflare Origin CA. A Cloudflare recomenda Full ou Full (strict) sempre que possível.Use Full (strict). It is the only common mode that encrypts the trip to the origin and checks the certificate. Full (strict) accepts a certificate from a public CA (such as Let’s Encrypt or an ACM public certificate) or from the Cloudflare Origin CA. Cloudflare recommends Full or Full (strict) whenever possible.

Forçar HTTPS no visitanteForce HTTPS for the visitor

Em SSL/TLS → Edge Certificates, ligue Always Use HTTPS: toda requisição HTTP é redirecionada para HTTPS na borda. A opção só aparece se o modo de criptografia não for Off. A Cloudflare recomenda não fazer esse redirecionamento na origem, porque pode causar loop de redirecionamento.Under SSL/TLS → Edge Certificates, turn on Always Use HTTPS: every HTTP request is redirected to HTTPS at the edge. The option only shows when the encryption mode is not Off. Cloudflare recommends not redirecting at the origin as well, because that can cause redirect loops.

CDN e cacheCDN and cache

Com o proxy ligado, a Cloudflare serve conteúdo de seus pontos de presença e aplica cache, WAF e proteção DDoS. Por padrão só alguns tipos de arquivo são cacheados; para cachear tudo é preciso uma regra de cache (Cache Everything). No R2, o cache só vale quando o bucket é acessado por um domínio personalizado (seção 4); a Cloudflare recomenda o Smart Tiered Cache nesse caso.With the proxy on, Cloudflare serves content from its points of presence and applies cache, WAF and DDoS protection. By default only certain file types are cached; to cache everything you need a cache rule (Cache Everything). On R2, the cache only applies when the bucket is reached through a custom domain (section 4); Cloudflare recommends Smart Tiered Cache in that case.

4. R2: bucket, chaves e acesso4. R2: bucket, keys and access

R2 é o storage de objetos da Cloudflare, com API compatível com S3. Por isso você o acessa com SDKs da AWS (@aws-sdk/client-s3, boto3) apontando o endpoint para a Cloudflare.R2 is Cloudflare’s object storage, with an S3-compatible API. That is why you reach it with AWS SDKs (@aws-sdk/client-s3, boto3) pointing the endpoint at Cloudflare.

Chaves de API (S3)API keys (S3)

  1. No painel do R2 (Overview), em Account Details, clique em Manage ao lado de API Tokens.In the R2 dashboard (Overview), under Account Details, click Manage next to API Tokens.
  2. Escolha o tipo: token de conta (vale até ser revogado) ou de usuário (fica inativo se você sair da conta).Choose the type: account token (valid until revoked) or user token (becomes inactive if you leave the account).
  3. Escolha a permissão e, se quiser, limite a buckets específicos: Object Read & Write (ler, gravar e listar objetos) é o suficiente para gerar URLs pré-assinadas.Choose the permission and, if you like, limit it to specific buckets: Object Read & Write (read, write and list objects) is enough to generate presigned URLs.
  4. Copie o Access Key ID e o Secret Access Key.Copy the Access Key ID and the Secret Access Key.
O Secret Access Key aparece uma única vez. Depois não dá para recuperá-lo; se perder, crie outro token.The Secret Access Key is shown only once. You cannot retrieve it afterwards; if you lose it, create another token.

O endpoint S3 é https://<ACCOUNT_ID>.r2.cloudflarestorage.com. Buckets com jurisdição (UE, EUA, FedRAMP) só respondem pelo endpoint correspondente, por exemplo <ACCOUNT_ID>.eu.r2.cloudflarestorage.com.The S3 endpoint is https://<ACCOUNT_ID>.r2.cloudflarestorage.com. Jurisdictional buckets (EU, US, FedRAMP) only answer on their matching endpoint, for example <ACCOUNT_ID>.eu.r2.cloudflarestorage.com.

Bucket privado e bucket públicoPrivate and public bucket

Todo bucket nasce privado: nunca é acessível publicamente sem uma ação explícita sua. Há duas formas de abrir leitura pública, e podem ser usadas separadamente:Every bucket starts private: it is never publicly accessible without an explicit action from you. There are two ways to open public reads, and they can be used independently:

Domínio personalizadoCustom domainSubdomínio r2.devr2.dev subdomain
UsoUseProduçãoProductionSó desenvolvimento; tem limite de requisiçõesDevelopment only; rate-limited
Cache / CDNCache / CDNSim (Cloudflare Cache)Yes (Cloudflare Cache)NãoNo
WAF, controle de acesso, Bot ManagementWAF, access control, Bot ManagementSimYesNãoNo
RequisitoRequirementDomínio é uma zona na mesma conta do bucketDomain is a zone in the same account as the bucketNenhumNone

Para conectar o domínio: bucket → Settings → Custom Domains → Add → informe o domínio (por exemplo media.exemplo.com) → revise o registro DNS → Connect Domain. Aguarde o status passar de Initializing a Active. A listagem do conteúdo na raiz do domínio não existe: só dá para acessar objetos por chave.To connect the domain: bucket → Settings → Custom Domains → Add → enter the domain (for example media.example.com) → review the DNS record → Connect Domain. Wait for the status to go from Initializing to Active. There is no listing of contents at the domain root: objects are only reachable by key.

Se você protege o bucket com WAF ou Cloudflare Access, desative também o r2.dev; do contrário o bucket continua público por ele. Para desativar: Public Development URL → Disable e digite disallow.If you protect the bucket with WAF or Cloudflare Access, disable r2.dev too; otherwise the bucket stays public through it. To disable it: Public Development URL → Disable and type disallow.

Quando usar cada umWhen to use each

  • Privado + URL pré-assinada: arquivos de usuário, documentos, qualquer coisa que exija decidir quem acessa e por quanto tempo.Private + presigned URL: user files, documents, anything where you must decide who gets access and for how long.
  • Público + domínio personalizado: imagens e vídeos do site, assets estáticos, tudo que é para qualquer pessoa e que se beneficia de CDN.Public + custom domain: site images and videos, static assets, anything meant for everyone that benefits from a CDN.

5. URLs pré-assinadas: o que são e como funcionam5. Presigned URLs: what they are and how they work

Uma URL pré-assinada dá acesso temporário a um único objeto sem expor suas chaves de API. A assinatura vai embutida na própria URL e é gerada no seu servidor, sem falar com o R2: o cálculo usa as suas chaves e o algoritmo AWS Signature Version 4.A presigned URL grants temporary access to a single object without exposing your API keys. The signature is embedded in the URL itself and is generated on your server, without contacting R2: the calculation uses your keys and the AWS Signature Version 4 algorithm.

O que a assinatura amarraWhat the signature binds

  • Recurso: conta, bucket e caminho do objeto.Resource: account, bucket and object path.
  • Operação: GET, PUT, HEAD ou DELETE. POST (formulário HTML) não é suportado.Operation: GET, PUT, HEAD or DELETE. POST (HTML form uploads) is not supported.
  • Validade: de 1 segundo a 7 dias (604.800 s).Expiry: from 1 second to 7 days (604,800 s).
  • Content-Type, se você o informar ao gerar uma URL de PUT: o cliente tem de enviar exatamente esse tipo.Content-Type, if you set it when generating a PUT URL: the client must send exactly that type.

Alterar qualquer um desses parâmetros na URL (ou enviar outro Content-Type) invalida a assinatura e o R2 responde 403 SignatureDoesNotMatch. Quando o R2 recebe a requisição, ele recalcula a assinatura com a mesma chave e compara; se a URL ainda estiver dentro da validade, executa a operação.Changing any of these parameters in the URL (or sending another Content-Type) invalidates the signature and R2 answers 403 SignatureDoesNotMatch. When R2 receives the request, it recomputes the signature with the same key and compares; if the URL is still within its validity, it performs the operation.

Trate a URL pré-assinada como um token de portador: quem a tiver pode executar a operação até ela expirar. Prefira validades curtas (por exemplo 10 minutos para upload) e entregue-a só a quem deve usá-la.Treat a presigned URL as a bearer token: anyone holding it can perform the operation until it expires. Prefer short validities (for example 10 minutes for uploads) and hand it only to whoever must use it.
URLs pré-assinadas funcionam só no domínio da API S3 (<ACCOUNT_ID>.r2.cloudflarestorage.com). Não funcionam em domínio personalizado. Para controlar acesso num domínio personalizado há a validação HMAC do WAF, a partir do plano Pro.Presigned URLs work only on the S3 API domain (<ACCOUNT_ID>.r2.cloudflarestorage.com). They do not work on a custom domain. To control access on a custom domain there is WAF HMAC validation, on the Pro plan or above.

Gerando no servidor (Node.js)Generating on the server (Node.js)

import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const r2 = new S3Client({
  region: "auto", // required by the SDK, not used by R2
  endpoint: process.env.R2_ENDPOINT,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
  },
});

export function signedDownloadUrl(key) {
  const command = new GetObjectCommand({ Bucket: process.env.R2_BUCKET, Key: key });
  return getSignedUrl(r2, command, { expiresIn: 3600 });
}

export function signedUploadUrl(key, contentType) {
  const command = new PutObjectCommand({
    Bucket: process.env.R2_BUCKET,
    Key: key,
    ContentType: contentType, // the browser must send exactly this Content-Type
  });
  return getSignedUrl(r2, command, { expiresIn: 600 });
}

O mesmo em PythonThe same in Python

import os, boto3

r2 = boto3.client(
    service_name="s3",
    endpoint_url=os.environ["R2_ENDPOINT"],
    aws_access_key_id=os.environ["R2_ACCESS_KEY_ID"],
    aws_secret_access_key=os.environ["R2_SECRET_ACCESS_KEY"],
    region_name="auto",  # required by the SDK, not used by R2
)

download_url = r2.generate_presigned_url(
    "get_object",
    Params={"Bucket": os.environ["R2_BUCKET"], "Key": "image.png"},
    ExpiresIn=3600,
)
upload_url = r2.generate_presigned_url(
    "put_object",
    Params={"Bucket": os.environ["R2_BUCKET"], "Key": "image.png", "ContentType": "image/png"},
    ExpiresIn=600,
)

A CLI da AWS (aws s3 presign) só gera URLs de GET; para PUT use um SDK.The AWS CLI (aws s3 presign) only generates GET URLs; for PUT use an SDK.

Fluxo de upload pelo navegadorBrowser upload flow

  1. O navegador pede ao seu backend uma URL de upload, informando o nome e o tipo do arquivo. O backend decide a chave do objeto e gera a URL.The browser asks your backend for an upload URL, sending the file name and type. The backend decides the object key and generates the URL.
  2. O navegador envia o arquivo direto ao R2 com PUT. O arquivo não passa pelo seu servidor.The browser sends the file straight to R2 with PUT. The file never goes through your server.
  3. O backend guarda a chave do objeto (não a URL) no banco. Quando alguém precisar ver o arquivo, gera uma URL de download nova.The backend stores the object key (not the URL) in the database. When someone needs to see the file, it generates a fresh download URL.
const { url } = await (await fetch("/api/uploads", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: file.name, type: file.type }),
})).json();

await fetch(url, {
  method: "PUT",
  headers: { "Content-Type": file.type }, // same value used to sign the URL
  body: file,
});

CORS no bucketCORS on the bucket

Como o navegador faz esse PUT para outra origem, o R2 precisa de uma política de CORS. Um PUT é sempre precedido de uma requisição preflight (OPTIONS); o navegador só envia o arquivo se o bucket autorizar a origem, o método e o cabeçalho. Em bucket → Settings → CORS Policy → Add CORS policy (aba JSON):Because the browser makes this PUT to another origin, R2 needs a CORS policy. A PUT is always preceded by a preflight request (OPTIONS); the browser only sends the file if the bucket allows the origin, the method and the header. Under bucket → Settings → CORS Policy → Add CORS policy (JSON tab):

[
  {
    "AllowedOrigins": ["https://app.example.com"],
    "AllowedMethods": ["PUT", "GET"],
    "AllowedHeaders": ["Content-Type"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]

Ao mudar a política de um domínio personalizado, limpe o cache do hostname (Cache Purge), porque respostas cacheadas podem não ter os novos cabeçalhos.When you change the policy of a custom domain, purge the hostname’s cache (Cache Purge), because cached responses may not carry the new headers.

6. Como o navegador renderiza foto e vídeo6. How the browser renders photos and video

Para o navegador, uma URL pré-assinada é uma URL comum. Ele não sabe de assinatura: faz um GET e usa o que vier na resposta.To the browser, a presigned URL is an ordinary URL. It knows nothing about signatures: it makes a GET and uses whatever comes back.

Foto: <img>Photo: <img>

<img src="https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/photo.jpg?X-Amz-Algorithm=…&X-Amz-Signature=…" alt="Description">
  1. O navegador encontra o <img> e dispara um GET para a URL, com a assinatura na query string.The browser finds the <img> and fires a GET to the URL, with the signature in the query string.
  2. O R2 confere a assinatura e a validade e responde 200 com o corpo do arquivo e o cabeçalho Content-Type (por exemplo image/jpeg).R2 checks the signature and the expiry and answers 200 with the file body and the Content-Type header (for example image/jpeg).
  3. O navegador decodifica os bytes conforme o tipo e desenha a imagem. Se a URL expirou ou foi alterada, a resposta é 403 e aparece a imagem quebrada.The browser decodes the bytes according to the type and paints the image. If the URL expired or was altered, the answer is 403 and a broken image appears.

Mostrar a imagem não exige CORS: elementos <img> e <video> carregam recursos de outra origem normalmente. O CORS passa a ser necessário quando o código lê os dados, por exemplo com fetch(), ou desenha a imagem ou um quadro de vídeo num <canvas>; nesse caso use o atributo crossorigin e configure o CORS do bucket.Showing the image does not require CORS: <img> and <video> elements load cross-origin resources normally. CORS becomes necessary when code reads the data, for example with fetch(), or draws the image or a video frame to a <canvas>; in that case use the crossorigin attribute and configure the bucket’s CORS.

Vídeo: <video>Video: <video>

<video controls preload="metadata" playsinline poster="poster.jpg">
  <source src="https://…signed-url…/clip.mp4" type="video/mp4">
</video>

O vídeo é diferente da foto porque o navegador não baixa tudo de uma vez. Ele usa requisições por faixa de bytes (HTTP Range):Video differs from a photo because the browser does not download everything at once. It uses byte-range requests (HTTP Range):

  1. Com preload="metadata" o navegador busca só o começo do arquivo, o bastante para saber a duração e as dimensões, e mostra o poster (ou o primeiro quadro).With preload="metadata" the browser fetches only the start of the file, enough to learn duration and dimensions, and shows the poster (or the first frame).
  2. O servidor precisa anunciar Accept-Ranges: bytes. O navegador então pede trechos com Range: bytes=0-1023 e recebe 206 Partial Content com Content-Range.The server must advertise Accept-Ranges: bytes. The browser then asks for chunks with Range: bytes=0-1023 and receives 206 Partial Content with Content-Range.
  3. Ao tocar o vídeo ou arrastar a barra de progresso, o navegador pede o trecho do ponto escolhido, sem baixar o que vem antes. Sem suporte a Range, o servidor manda o arquivo inteiro com 200 e não dá para pular com eficiência.When you play or drag the progress bar, the browser asks for the chunk at the chosen point, without downloading what comes before. Without Range support the server sends the whole file with 200 and seeking is not efficient.

O R2 aceita o cabeçalho Range em GetObject, então vídeo em bucket R2 pode ser buscado por trechos.R2 supports the Range header on GetObject, so video in an R2 bucket can be fetched in chunks.

Consequência das URLs pré-assinadas: cada requisição por faixa é uma requisição nova com a mesma URL. Se a URL expirar durante a reprodução, os próximos trechos falham com 403 e o vídeo trava. Gere a URL com validade maior que a duração do vídeo.Consequence for presigned URLs: each range request is a new request with the same URL. If the URL expires during playback, the next chunks fail with 403 and the video stalls. Generate the URL with a validity longer than the video’s duration.

Content-Type, o detalhe que mais importaContent-Type, the detail that matters most

  • O navegador decide como decodificar pelo Content-Type da resposta. Se o tipo estiver errado ou faltar, a mídia pode não aparecer; vídeos podem exibir um quadro cinza com um X.The browser decides how to decode by the response’s Content-Type. If the type is wrong or missing, the media may not show; videos can display a grey box with an X.
  • No R2 o tipo vem dos metadados gravados no upload (PutObject aceita Content-Type, Cache-Control e Content-Disposition). Por isso assine o PUT com o ContentType correto, como no exemplo da seção 5.On R2 the type comes from the metadata stored at upload (PutObject accepts Content-Type, Cache-Control and Content-Disposition). That is why you sign the PUT with the correct ContentType, as in the example in section 5.
  • No <video>, o atributo type do <source> (video/mp4, video/webm) deixa o navegador pular formatos que não toca, sem baixá-los.On <video>, the type attribute of <source> (video/mp4, video/webm) lets the browser skip formats it cannot play, without downloading them.
  • Content-Disposition: attachment pede ao navegador que baixe o arquivo em vez de exibi-lo; inline (padrão) o exibe. Para mídia exibida em <img>/<video>, evite attachment e teste nos navegadores que você atende: a documentação do MDN descreve o cabeçalho para páginas e downloads, não para esses elementos.Content-Disposition: attachment asks the browser to download the file instead of displaying it; inline (the default) displays it. For media shown in <img>/<video>, avoid attachment and test in the browsers you support: MDN describes the header for pages and downloads, not for these elements.

Presignado ou público: o que muda na renderizaçãoPresigned or public: what changes in rendering

URL pré-assinada (privado)Presigned URL (private)Domínio personalizado (público)Custom domain (public)
EndereçoAddress…r2.cloudflarestorage.com/chave?X-Amz-…, muda a cada geração…r2.cloudflarestorage.com/key?X-Amz-…, changes on every generationhttps://media.exemplo.com/chave, estávelhttps://media.example.com/key, stable
Expira?Expires?Sim, até 7 diasYes, up to 7 daysNãoNo
CDN da CloudflareCloudflare CDNNão (domínio da API S3)No (S3 API domain)SimYes
Quem pode verWho can seeQuem tiver a URL, até expirarWhoever has the URL, until it expiresQualquer pessoaAnyone

7. Variáveis, valores e chaves7. Variables, values and keys

O que cada parte precisa e onde obter. Segredos vão para o AWS Secrets Manager ou o Parameter Store (veja as notas de AWS), nunca para o repositório.What each part needs and where to get it. Secrets go to AWS Secrets Manager or Parameter Store (see the AWS notes), never into the repository.

Variável / valorVariable / valueO que é e onde obterWhat it is and where to get itSegredo?Secret?
R2_ACCOUNT_IDID da conta Cloudflare, mostrado no painel do R2. Compõe o endpoint.Cloudflare account ID, shown in the R2 dashboard. Part of the endpoint.NãoNo
R2_ENDPOINThttps://<ACCOUNT_ID>.r2.cloudflarestorage.com (com jurisdição: .eu., .us., .fedramp.).https://<ACCOUNT_ID>.r2.cloudflarestorage.com (with a jurisdiction: .eu., .us., .fedramp.).NãoNo
R2_ACCESS_KEY_IDGerado em R2 → API Tokens (seção 4).Generated under R2 → API Tokens (section 4).SimYes
R2_SECRET_ACCESS_KEYGerado junto com o Access Key ID; exibido uma única vez.Generated together with the Access Key ID; shown only once.SimYes
R2_BUCKETNome do bucket criado no R2.Name of the bucket created in R2.NãoNo
R2_PRESIGN_EXPIRES_SECONDSValidade das URLs. De 1 a 604.800 (7 dias). Use valores curtos.URL validity. From 1 to 604,800 (7 days). Use short values.NãoNo
R2_PUBLIC_BASE_URLDomínio personalizado do bucket público (https://media.exemplo.com).Custom domain of the public bucket (https://media.example.com).NãoNo
Região do SDK = autoSDK region = autoObrigatória pelo SDK, mas o R2 não a usa.Required by the SDK, but R2 does not use it.NãoNo
origin.pemCertificado de origem (Cloudflare Origin CA), parte pública (seção 9).Origin certificate (Cloudflare Origin CA), public part (section 9).NãoNo
origin.keyChave privada do certificado de origem; só aparece na criação.Private key of the origin certificate; only shown at creation.SimYes
CERTIFICATE_ARNARN devolvido pelo aws acm import-certificate, usado no listener HTTPS.ARN returned by aws acm import-certificate, used on the HTTPS listener.NãoNo
ALB_ARN, TARGET_GROUP_ARNARNs do load balancer e do target group na AWS.ARNs of the load balancer and target group on AWS.NãoNo

Exemplo de arquivo de ambiente (backend)Example environment file (backend)

# --- Cloudflare R2 (backend) ---
R2_ACCOUNT_ID=<ACCOUNT_ID>
R2_ENDPOINT=https://<ACCOUNT_ID>.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=<ACCESS_KEY_ID>
R2_SECRET_ACCESS_KEY=<SECRET_ACCESS_KEY>
R2_BUCKET=<BUCKET_NAME>
R2_PRESIGN_EXPIRES_SECONDS=600
# only if the bucket has a public custom domain
R2_PUBLIC_BASE_URL=https://media.example.com
Não grave o .env no Git. No ECS, injete os segredos como secrets da task definition, vindos do Secrets Manager ou do Parameter Store.Do not commit the .env to Git. On ECS, inject the secrets as task definition secrets, coming from Secrets Manager or Parameter Store.

O token de API do R2 pode ser limitado a buckets específicos (permissões Object). Crie um token por ambiente (develop e produção), cada um com acesso só ao bucket daquele ambiente.The R2 API token can be limited to specific buckets (Object permissions). Create one token per environment (develop and production), each with access only to that environment’s bucket.

8. O que cada recurso precisa para funcionar8. What each feature needs to work

RecursoFeaturePrecisa deNeeds
Domínio funcionandoWorking domainRegistro/NS na Cloudflare, zona ativa, registro DNS com proxy.Registration/NS on Cloudflare, active zone, a proxied DNS record.
HTTPS para o visitanteHTTPS for the visitorZona ativa (Universal SSL) e Always Use HTTPS.Active zone (Universal SSL) and Always Use HTTPS.
HTTPS até a AWSHTTPS to AWSModo Full (strict), certificado no ALB (seção 9), listener 443, SG liberando 443.Full (strict) mode, a certificate on the ALB (section 9), a 443 listener, a security group allowing 443.
Upload por URL pré-assinadaPresigned uploadChaves S3 do R2, endpoint, bucket, backend que assina o PUT com ContentType, CORS com PUT e Content-Type.R2 S3 keys, endpoint, bucket, a backend that signs the PUT with ContentType, CORS with PUT and Content-Type.
Download por URL pré-assinadaPresigned downloadChaves S3, endpoint, bucket, backend que assina o GET com validade suficiente.S3 keys, endpoint, bucket, a backend that signs the GET with enough validity.
Mídia pública com CDNPublic media with CDNDomínio personalizado no bucket (zona na mesma conta) e regra de cache, se quiser cachear todos os tipos.Custom domain on the bucket (zone in the same account) and a cache rule, if you want every type cached.

9. Certificado no ALB: HTTPS até a AWS9. Certificate on the ALB: HTTPS all the way to AWS

Com o modo Full (strict), a Cloudflare conecta ao ALB por HTTPS e valida o certificado. O ALB precisa, então, de um certificado válido para o hostname (por exemplo app.exemplo.com) num listener HTTPS. Há dois caminhos; este usa o certificado de origem gratuito da Cloudflare.With Full (strict) mode, Cloudflare connects to the ALB over HTTPS and validates the certificate. The ALB therefore needs a valid certificate for the hostname (for example app.example.com) on an HTTPS listener. There are two paths; this one uses Cloudflare’s free origin certificate.

Passo 1: criar o certificado de origem na CloudflareStep 1: create the origin certificate on Cloudflare

  1. Painel → seu domínio → SSL/TLS → Origin Server → Origin Certificates → Create Certificate.Dashboard → your domain → SSL/TLS → Origin Server → Origin Certificates → Create Certificate.
  2. Deixe a Cloudflare gerar a chave privada e o CSR, e escolha o tipo da chave (RSA ou ECC).Let Cloudflare generate the private key and CSR, and choose the key type (RSA or ECC).
  3. Liste os hostnames cobertos (a raiz e o curinga de primeiro nível vêm por padrão) e o prazo de validade.List the covered hostnames (the apex and the first-level wildcard come by default) and the validity period.
  4. Escolha o formato PEM e salve o certificado como origin.pem e a chave privada como origin.key.Choose the PEM format and save the certificate as origin.pem and the private key as origin.key.
A chave privada não pode ser vista depois de você sair da tela. Guarde-a num cofre de segredos. A Cloudflare também não avisa quando o certificado de origem está perto de expirar: anote a data.The private key cannot be viewed afterwards once you leave the screen. Store it in a secrets vault. Cloudflare also does not notify you when the origin certificate is close to expiring: note the date.
Esse certificado só é confiável para a Cloudflare. Navegadores não confiam nele: se você pausar o proxy ou deixar o registro como DNS only, os visitantes verão erro de certificado. Por isso o registro do ALB fica sempre com proxy.This certificate is only trusted by Cloudflare. Browsers do not trust it: if you pause the proxy or leave the record as DNS only, visitors will see a certificate error. That is why the ALB record always stays proxied.

Passo 2: importar para o ACMStep 2: import into ACM

O ACM exige certificado e chave em PEM, e a chave sem senha (iniciando em -----BEGIN PRIVATE KEY-----, com no máximo 5 KB). O ALB aceita todos os algoritmos do ACM (RSA 1024 a 4096 e ECDSA 256, 384 e 521). O certificado precisa estar dentro da validade no momento da importação. Importe na mesma região do load balancer.ACM requires the certificate and key in PEM, and the key without a passphrase (starting with -----BEGIN PRIVATE KEY-----, up to 5 KB). The ALB accepts every ACM algorithm (RSA 1024 to 4096 and ECDSA 256, 384 and 521). The certificate must be inside its validity period at import time. Import in the same Region as the load balancer.

aws acm import-certificate \
  --certificate fileb://origin.pem \
  --private-key fileb://origin.key
# prints the certificate ARN: save it as CERTIFICATE_ARN

A cadeia (--certificate-chain) é opcional. Se for usar, ela deve estar em PEM, em ordem, sem repetir o seu certificado. Também dá para importar pelo console: ACM → Import a certificate e colar o corpo, a chave privada e, se houver, a cadeia.The chain (--certificate-chain) is optional. If you use it, it must be PEM, in order, without repeating your own certificate. You can also import through the console: ACM → Import a certificate and paste the body, the private key and, if any, the chain.

Passo 3: listener HTTPS no ALBStep 3: HTTPS listener on the ALB

aws elbv2 create-listener \
  --load-balancer-arn <ALB_ARN> \
  --protocol HTTPS \
  --port 443 \
  --default-actions Type=forward,TargetGroupArn=<TARGET_GROUP_ARN> \
  --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
  --certificates CertificateArn=<CERTIFICATE_ARN>

No console: EC2 → Load Balancers → seu ALB → Listeners and rules → Add listener, protocolo HTTPS, porta 443, ação Forward to target groups, política de segurança e, em Default SSL/TLS certificate, o certificado do ACM importado. O ALB usa esse certificado para encerrar a conexão do cliente (a Cloudflare, neste caso) e então repassa a requisição aos alvos. O ALB não suporta chaves ED25519.In the console: EC2 → Load Balancers → your ALB → Listeners and rules → Add listener, protocol HTTPS, port 443, action Forward to target groups, a security policy and, under Default SSL/TLS certificate, the imported ACM certificate. The ALB uses that certificate to terminate the client connection (Cloudflare, in this case) and then forwards the request to the targets. The ALB does not support ED25519 keys.

Passo 4: nada em HTTP puroStep 4: nothing over plain HTTP

Para garantir que nada chegue ao ALB sem criptografia, libere no security group do ALB só a porta 443 e, se mantiver a porta 80, faça dela um redirecionamento permanente para HTTPS:To make sure nothing reaches the ALB unencrypted, allow only port 443 in the ALB’s security group and, if you keep port 80, make it a permanent redirect to HTTPS:

aws elbv2 create-listener \
  --load-balancer-arn <ALB_ARN> \
  --protocol HTTP \
  --port 80 \
  --default-actions '[{
    "Type": "redirect",
    "RedirectConfig": {
      "Protocol": "HTTPS",
      "Port": "443",
      "Host": "#{host}",
      "Path": "/#{path}",
      "Query": "#{query}",
      "StatusCode": "HTTP_301"
    }
  }]'

Como o modo Full (strict) já faz a Cloudflare falar HTTPS com a origem, esse redirecionamento no ALB não cria loop; ele só protege contra acessos que ignorem a Cloudflare.Because Full (strict) mode already makes Cloudflare speak HTTPS to the origin, this redirect on the ALB does not create a loop; it only protects against access that bypasses Cloudflare.

Passo 5: ligar a Cloudflare ao ALBStep 5: connect Cloudflare to the ALB

  1. Em DNS → Records, crie um CNAME app apontando para o nome DNS do ALB, com proxy ligado.Under DNS → Records, create a CNAME app pointing to the ALB DNS name, with the proxy on.
  2. Em SSL/TLS → Overview, escolha Full (strict).Under SSL/TLS → Overview, choose Full (strict).
  3. Em Edge Certificates, ligue Always Use HTTPS.Under Edge Certificates, turn on Always Use HTTPS.
  4. Teste: curl -I https://app.exemplo.com deve responder pela Cloudflare, e http://app.exemplo.com deve redirecionar para HTTPS.Test: curl -I https://app.example.com should answer through Cloudflare, and http://app.example.com should redirect to HTTPS.

Resultado: visitante → Cloudflare em HTTPS (certificado de borda) e Cloudflare → ALB em HTTPS (certificado de origem validado). A Cloudflare conecta ao seu domínio nas portas HTTPS que ela proxia (a 443 está entre elas).Result: visitor → Cloudflare over HTTPS (edge certificate) and Cloudflare → ALB over HTTPS (validated origin certificate). Cloudflare proxies the HTTPS ports it supports (443 is among them).

Alternativa: certificado público do ACMAlternative: ACM public certificate

O Full (strict) também aceita certificados de CA pública. Um certificado público do ACM é gratuito e se renova sozinho, mas a validação é por DNS: o ACM fornece um CNAME de validação que você cria na Cloudflare como DNS only. Esse caminho evita ter de controlar a expiração do certificado de origem (veja a seção ACM das notas de AWS).Full (strict) also accepts certificates from a public CA. An ACM public certificate is free and renews itself, but validation is by DNS: ACM gives you a validation CNAME that you create on Cloudflare as DNS only. This path avoids having to track the origin certificate’s expiry (see the ACM section of the AWS notes).

RenovaçãoRenewal

  • Certificado de origem da Cloudflare: sem aviso de expiração. Renove-o criando outro e reimportando-o no ACM; a reimportação não pode remover extensões KeyUsage/ExtendedKeyUsage que o certificado anterior tinha (com exceções descritas nas docs do ACM).Cloudflare origin certificate: no expiry notice. Renew it by creating another and reimporting it into ACM; a reimport cannot remove KeyUsage/ExtendedKeyUsage extensions that the previous certificate had (with exceptions described in the ACM docs).
  • Certificado de borda (Universal SSL): a Cloudflare cuida da emissão, renovação e instalação.Edge certificate (Universal SSL): Cloudflare handles issuance, renewal and installation.
Nota pessoal, não verificada nas fontes: o trecho ALB → alvos (ECS, EC2) pode seguir em HTTP dentro da VPC ou ser HTTPS se o target group usar esse protocolo; as páginas consultadas não detalham isso. Se “100% HTTPS” inclui esse último salto, confirme nas docs de target groups do ELB. As páginas da Cloudflare consultadas também não detalham como o modo Full (strict) confere o hostname e a expiração do certificado de origem; use um certificado válido para o hostname.Personal note, not verified in the sources: the ALB → targets (ECS, EC2) leg can stay HTTP inside the VPC or be HTTPS if the target group uses that protocol; the pages consulted do not detail this. If “100% HTTPS” includes that last hop, confirm in the ELB target group docs. The Cloudflare pages consulted also do not detail how Full (strict) checks the origin certificate’s hostname and expiry; use a certificate valid for the hostname.

10. Problemas comuns10. Common problems

  • 403 SignatureDoesNotMatch no upload: o Content-Type do PUT é diferente do usado para assinar, ou a URL foi alterada.403 SignatureDoesNotMatch on upload: the PUT’s Content-Type differs from the one used to sign, or the URL was altered.
  • Erro de CORS no upload: falta a política no bucket, ou ela não inclui a origem, o método PUT ou o cabeçalho Content-Type.CORS error on upload: the bucket policy is missing, or it does not include the origin, the PUT method or the Content-Type header.
  • URL pré-assinada não funciona no meu domínio: ela só funciona no domínio da API S3, não em domínio personalizado.Presigned URL does not work on my domain: it only works on the S3 API domain, not on a custom domain.
  • Vídeo não pula ou trava: a URL expirou durante a reprodução, ou o servidor não responde a Range com 206.Video does not seek or stalls: the URL expired during playback, or the server does not answer Range with 206.
  • Mídia não aparece: Content-Type errado ou ausente no objeto.Media does not show: wrong or missing Content-Type on the object.
  • Erro 524: a origem demorou mais que o Proxy Read Timeout da Cloudflare para responder.Error 524: the origin took longer than Cloudflare’s Proxy Read Timeout to answer.
  • Domínio parou de resolver: a verificação do e-mail do registrante expirou e a ICANN pôs o domínio em hold.Domain stopped resolving: registrant email verification expired and ICANN put the domain on hold.
  • Domínio personalizado do bucket preso em Initializing: use Retry connection; confira se a zona está na mesma conta do bucket.Bucket custom domain stuck on Initializing: use Retry connection; check that the zone is in the same account as the bucket.