Creación de URL firmadas para Amazon CloudFront

32

Versión corta: ¿Cómo puedo crear URL firmadas "a pedido" para imitar el comportamiento X-Accel-Redirect de Nginx (es decir, proteger las descargas) con Amazon CloudFront / S3 usando Python?

Tengo un servidor Django en funcionamiento con un front-end Nginx. Me han machacado con solicitudes y recientemente tuve que instalarlo como una aplicación Tornado WSGI para evitar que se bloquee en el modo FastCGI.

Ahora tengo un problema con mi servidor atascado (es decir, la mayor parte de su ancho de banda se está agotando) debido a que se le han realizado demasiadas solicitudes de medios, he estado buscando CDN y creo que Amazon CloudFront / S3 sería la solución adecuada para mí.

He estado usando el encabezado X-Accel-Redirect de Nginx para proteger los archivos de descargas no autorizadas, pero no tengo esa capacidad con CloudFront / S3; sin embargo, ofrecen URL firmadas. No soy un experto en Python y definitivamente no sé cómo crear una URL firmada correctamente, por lo que esperaba que alguien tuviera un enlace sobre cómo hacer estas URL "a pedido" o que estuviera dispuesto a explicar cómo aquí, sería muy apreciado.

Además, ¿es esta la solución adecuada, incluso? No estoy muy familiarizado con las CDN, ¿hay alguna CDN que sea más adecuada para esto?

1
  • Por cierto, cambie a uWSGI y su FastCGI no se bloqueará más. 11/06/10 a las 22:05
36

Las URL firmadas de Amazon CloudFront funcionan de manera diferente a las URL firmadas de Amazon S3. CloudFront utiliza firmas RSA basadas en un par de claves de CloudFront separado que debe configurar en la página Credenciales de su cuenta de Amazon. Aquí hay un código para generar una URL de tiempo limitado en Python usando la biblioteca M2Crypto :

Cree un par de claves para CloudFront

Creo que la única forma de hacerlo es a través del sitio web de Amazon. Vaya a la página "Cuenta" de AWS y haga clic en el enlace "Credenciales de seguridad". Haga clic en la pestaña "Pares de claves" y luego haga clic en "Crear un nuevo par de claves". Esto generará un nuevo par de claves y descargará automáticamente un archivo de clave privada (pk-xxxxxxxxx.pem). Mantenga el archivo de claves seguro y privado. También anote el "ID del par de claves" de amazon, ya que lo necesitaremos en el siguiente paso.

Genera algunas URL en Python

A partir de la versión 2.0 de boto, no parece haber ningún soporte para generar URL de CloudFront firmadas. Python no incluye rutinas de cifrado RSA en la biblioteca estándar, por lo que tendremos que usar una biblioteca adicional. He usado M2Crypto en este ejemplo.

Para una distribución sin transmisión, debe usar la URL completa de la nube como recurso; sin embargo, para la transmisión solo usamos el nombre de objeto del archivo de video. Consulte el código a continuación para ver un ejemplo completo de cómo generar una URL que solo dura 5 minutos.

Este código se basa libremente en el código de ejemplo PHP proporcionado por Amazon en la documentación de CloudFront.

from M2Crypto import EVP
import base64
import time

def aws_url_base64_encode(msg):
    msg_base64 = base64.b64encode(msg)
    msg_base64 = msg_base64.replace('+', '-')
    msg_base64 = msg_base64.replace('=', '_')
    msg_base64 = msg_base64.replace('/', '~')
    return msg_base64

def sign_string(message, priv_key_string):
    key = EVP.load_key_string(priv_key_string)
    key.reset_context(md='sha1')
    key.sign_init()
    key.sign_update(message)
    signature = key.sign_final()
    return signature

def create_url(url, encoded_signature, key_pair_id, expires):
    signed_url = "%(url)s?Expires=%(expires)s&Signature=%(encoded_signature)s&Key-Pair-Id=%(key_pair_id)s" % {
            'url':url,
            'expires':expires,
            'encoded_signature':encoded_signature,
            'key_pair_id':key_pair_id,
            }
    return signed_url

def get_canned_policy_url(url, priv_key_string, key_pair_id, expires):
    #we manually construct this policy string to ensure formatting matches signature
    canned_policy = '{"Statement":[{"Resource":"%(url)s","Condition":{"DateLessThan":{"AWS:EpochTime":%(expires)s}}}]}' % {'url':url, 'expires':expires}

    #sign the non-encoded policy
    signature = sign_string(canned_policy, priv_key_string)
    #now base64 encode the signature (URL safe as well)
    encoded_signature = aws_url_base64_encode(signature)

    #combine these into a full url
    signed_url = create_url(url, encoded_signature, key_pair_id, expires);

    return signed_url

def encode_query_param(resource):
    enc = resource
    enc = enc.replace('?', '%3F')
    enc = enc.replace('=', '%3D')
    enc = enc.replace('&', '%26')
    return enc


#Set parameters for URL
key_pair_id = "APKAIAZVIO4BQ" #from the AWS accounts CloudFront tab
priv_key_file = "cloudfront-pk.pem" #your private keypair file
# Use the FULL URL for non-streaming:
resource = "http://34254534.cloudfront.net/video.mp4"
#resource = 'video.mp4' #your resource (just object name for streaming videos)
expires = int(time.time()) + 300 #5 min

#Create the signed URL
priv_key_string = open(priv_key_file).read()
signed_url = get_canned_policy_url(resource, priv_key_string, key_pair_id, expires)

print(signed_url)

#Flash player doesn't like query params so encode them if you're using a streaming distribution
#enc_url = encode_query_param(signed_url)
#print(enc_url)

Asegúrese de configurar su distribución con un parámetro TrustedSigners establecido en la cuenta que tiene su par de claves (o "Self" si es su propia cuenta)

Consulte Introducción a la transmisión segura de AWS CloudFront con Python para obtener un ejemplo completamente elaborado sobre cómo configurar esto para la transmisión con Python.

4
  • dentro de get_canned_policy_urlusted establece la política codificada en una encoded_policyvariable, pero nunca la use. ¿Se supone que debe estar ahí? 19 de enero de 2012 a las 4:01
  • 2
    Hola MattoTodd, tienes razón, eso no necesita estar ahí. Además, boto v2.1 ahora es compatible con esto de forma nativa. Consulte secretmike.com/2011/10/aws-cloudfront-secure-streaming.html para ver cómo funciona con el nuevo código boto. Una vez que la versión de boto haya estado disponible por un tiempo, actualizaré estas respuestas. 19/01/12 a las 16:44
  • 1
    ¿Hay algún ejemplo que utilice el método create_signed_url y Boto 2.5.2? 30/06/12 a las 0:06
  • 1
    @ipegasus: agregué un ejemplo create_signed_url()en una respuesta separada , es realmente bastante simple ahora debido al esfuerzo de @ secretmike para incorporar su solución en boto, ¡muchas gracias por esto! 22/04/13 a las 11:09
27

Esta función ya es compatible con Botocore , que es la biblioteca subyacente de Boto3, el último AWS SDK oficial para Python . (El siguiente ejemplo requiere la instalación del paquete rsa, pero también puede usar otro paquete RSA, simplemente defina su propio "firmante RSA normalizado").

El uso se ve así:

    from botocore.signers import CloudFrontSigner
    # First you create a cloudfront signer based on a normalized RSA signer::
    import rsa
    def rsa_signer(message):
        private_key = open('private_key.pem', 'r').read()
        return rsa.sign(
            message,
            rsa.PrivateKey.load_pkcs1(private_key.encode('utf8')),
            'SHA-1')  # CloudFront requires SHA-1 hash
    cf_signer = CloudFrontSigner(key_id, rsa_signer)

    # To sign with a canned policy::
    signed_url = cf_signer.generate_presigned_url(
        url, date_less_than=datetime(2015, 12, 1))

    # To sign with a custom policy::
    signed_url = cf_signer.generate_presigned_url(url, policy=my_policy)

Descargo de responsabilidad: soy el autor de ese PR.

2
  • Usé esta API para convertir una URL servida por CF en una URL firmada por CF que me da como https: // <mycf.cloudfront.net/ <my_s3_bucket_obj>? Expires = 1483228800 & Signature = <signature> && Key-Pair-Id = my_key_id. Sin embargo, la URL nunca caduca. La URL base real https: // <mycf.cloudfront.net/ <my_s3_bucket_obj> ya funciona como está. De hecho, probé una fecha antigua como datetime (2015, 12, 1). Sin embargo, la URL nunca caduca. ¿Hay algo que me falte para configurar en el CF? 18 de marzo de 2018 a las 6:28
  • @SoundaR Esa es una pregunta diferente, y el lugar más apropiado para esa pregunta es en problemas de github de AWS Boto3 y / o en el foro de soporte de Amazon.
    RayLuo
    18/03/18 a las 18:20
15

Como muchos ya han comentado, la respuesta inicialmente aceptada no se aplica a Amazon CloudFront, de hecho, en la medida en que el servicio de contenido privado a través de CloudFront requiere el uso de URL firmadas de CloudFront dedicadas ; en consecuencia, la respuesta de secretmike ha sido correcta, pero mientras tanto está desactualizada después de que él mismo se tomó el tiempo y se agregó soporte para generar URL firmadas para CloudFront (¡muchas gracias por esto!).

boto ahora admite un método create_signed_url dedicado y la antigua dependencia binaria M2Crypto también ha sido reemplazada recientemente por una implementación pura de Python RSA , consulte No use M2Crypto para la firma de URL frente a la nube .

Como es cada vez más común, uno puede encontrar uno o más ejemplos de buen uso dentro de las pruebas unitarias relacionadas (ver test_signed_urls.py ), por ejemplo test_canned_policy (self) - vea setUp (self) para las variables referenciadas self.pk_idy self.pk_str(obviamente necesitará su propio teclas):

def test_canned_policy(self):
    """
    Generate signed url from the Example Canned Policy in Amazon's
    documentation.
    """
    url = "http://d604721fxaaqy9.cloudfront.net/horizon.jpg?large=yes&license=yes"
    expire_time = 1258237200
    expected_url = "http://example.com/" # replaced for brevity
    signed_url = self.dist.create_signed_url(
        url, self.pk_id, expire_time, private_key_string=self.pk_str)
    # self.assertEqual(expected_url, signed_url)
0

La respuesta de secretmike funciona, pero es mejor usar en rsalugar de M2Crypto.

Usé botoqué usos rsa.

import boto
from boto.cloudfront import CloudFrontConnection
from boto.cloudfront.distribution import Distribution

expire_time = int(time.time() +3000)
conn = CloudFrontConnection('ACCESS_KEY_ID', 'SECRET_ACCESS_KEY')

##enter the id or domain name to select a distribution
distribution = Distribution(connection=conn, config=None, domain_name='', id='', last_modified_time=None, status='')
signed_url = distribution.create_signed_url(url='YOUR_URL', keypair_id='YOUR_KEYPAIR_ID_example-APKAIAZVIO4BQ',expire_time=expire_time,private_key_file="YOUR_PRIVATE_KEY_FILE_LOCATION")

Utilizar el boto documentation

1
  • No parece que se haya exportado ninguna política en firmado_url. En cualquier caso, no pude hacer que esto funcionara. 20/04/2015 a las 17:24
0

Esto es lo que uso para crear una política de modo que pueda dar acceso a varios archivos con la misma "firma":

import json 
import rsa
import time                                                                                                                                                                           

from base64 import b64encode 

url = "http://your_domain/*"                                                                                                                                                                      
expires = int(time.time() + 3600)

pem = """-----BEGIN RSA PRIVATE KEY-----  
...
-----END RSA PRIVATE KEY-----"""

key_pair_id = 'ABX....'

policy = {}                                                                                                                                                                           
policy['Statement'] = [{}]                                                                                                                                                            
policy['Statement'][0]['Resource'] = url                                                                                                                                              
policy['Statement'][0]['Condition'] = {}                                                                                                                                              
policy['Statement'][0]['Condition']['DateLessThan'] = {}                                                                                                                              
policy['Statement'][0]['Condition']['DateLessThan']['AWS:EpochTime'] = expires                                                                                                        

policy = json.dumps(policy)

private_key = rsa.PrivateKey.load_pkcs1(pem)                                                                                                                                          
signature = b64encode(rsa.sign(str(policy), private_key, 'SHA-1'))

print '?Policy=%s&Signature=%s&Key-Pair-Id=%s' % (b64encode(policy),                                                                                                                             
                                                  signature,                                                                                                                          
                                                  key_pair_id)

Puedo usarlo para todos los archivos, http://your_domain/*por ejemplo:

 http://your_domain/image2.png?Policy...
 http://your_domain/image2.png?Policy...
 http://your_domain/file1.json?Policy...
0

Encuentro soluciones sencillas, no necesito cambiar de s3.generate_urlforma,

simplemente seleccione su configuración Cloudfront: Yes, Update bucket policy.

Después de ese cambio de:

https://xxxx.s3.amazonaws.com/hello.png&Signature=sss&Expires=1585008320&AWSAccessKeyId=kkk

para

https://yyy.cloudfront.net/hello.png&Signature=sss&Expires=1585008320&AWSAccessKeyId=kkk

con yyy.cloudfront.netes su dominio de CloudFront

consulte: https://aws.amazon.com/blogs/developer/accessing-private-content-in-amazon-cloudfront/

ingrese la descripción de la imagen aquí