Getting started with secure AWS CloudFront streaming with Python Getting started with secure AWS CloudFront streaming with Python python python

Getting started with secure AWS CloudFront streaming with Python


You're right, it takes a lot of API work to get this set up. I hope they get it in the AWS Console soon!

UPDATE: I have submitted this code to boto - as of boto v2.1 (released 2011-10-27) this gets much easier. For boto < 2.1, use the instructions here. For boto 2.1 or greater, get the updated instructions on my blog: http://www.secretmike.com/2011/10/aws-cloudfront-secure-streaming.html Once boto v2.1 gets packaged by more distros I'll update the answer here.

To accomplish what you want you need to perform the following steps which I will detail below:

  1. Create your s3 bucket and upload some objects (you've already done this)
  2. Create a Cloudfront "Origin Access Identity" (basically an AWS account to allow cloudfront to access your s3 bucket)
  3. Modify the ACLs on your objects so that only your Cloudfront Origin Access Identity is allowed to read them (this prevents people from bypassing Cloudfront and going direct to s3)
  4. Create a cloudfront distribution with basic URLs and one which requires signed URLs
  5. Test that you can download objects from basic cloudfront distribution but not from s3 or the signed cloudfront distribution
  6. Create a key pair for signing URLs
  7. Generate some URLs using Python
  8. Test that the signed URLs work

1 - Create Bucket and upload object

The easiest way to do this is through the AWS Console but for completeness I'll show how using boto. Boto code is shown here:

import boto#credentials stored in environment AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEYs3 = boto.connect_s3()#bucket name MUST follow dns guidelinesnew_bucket_name = "stream.example.com"bucket = s3.create_bucket(new_bucket_name)object_name = "video.mp4"key = bucket.new_key(object_name)key.set_contents_from_filename(object_name)

2 - Create a Cloudfront "Origin Access Identity"

For now, this step can only be performed using the API. Boto code is here:

import boto#credentials stored in environment AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEYcf = boto.connect_cloudfront()oai = cf.create_origin_access_identity(comment='New identity for secure videos')#We need the following two values for later steps:print("Origin Access Identity ID: %s" % oai.id)print("Origin Access Identity S3CanonicalUserId: %s" % oai.s3_user_id)

3 - Modify the ACLs on your objects

Now that we've got our special S3 user account (the S3CanonicalUserId we created above) we need to give it access to our s3 objects. We can do this easily using the AWS Console by opening the object's (not the bucket's!) Permissions tab, click the "Add more permissions" button, and pasting the very long S3CanonicalUserId we got above into the "Grantee" field of a new. Make sure you give the new permission "Open/Download" rights.

You can also do this in code using the following boto script:

import boto#credentials stored in environment AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEYs3 = boto.connect_s3()bucket_name = "stream.example.com"bucket = s3.get_bucket(bucket_name)object_name = "video.mp4"key = bucket.get_key(object_name)#Now add read permission to our new s3 accounts3_canonical_user_id = "<your S3CanonicalUserID from above>"key.add_user_grant("READ", s3_canonical_user_id)

4 - Create a cloudfront distribution

Note that custom origins and private distributions are not fully supported in boto until version 2.0 which has not been formally released at time of writing. The code below pulls out some code from the boto 2.0 branch and hacks it together to get it going but it's not pretty. The 2.0 branch handles this much more elegantly - definitely use that if possible!

import botofrom boto.cloudfront.distribution import DistributionConfigfrom boto.cloudfront.exception import CloudFrontServerErrorimport redef get_domain_from_xml(xml):    results = re.findall("<DomainName>([^<]+)</DomainName>", xml)    return results[0]#custom class to hack this until boto v2.0 is releasedclass HackedStreamingDistributionConfig(DistributionConfig):    def __init__(self, connection=None, origin='', enabled=False,                 caller_reference='', cnames=None, comment='',                 trusted_signers=None):        DistributionConfig.__init__(self, connection=connection,                                    origin=origin, enabled=enabled,                                    caller_reference=caller_reference,                                    cnames=cnames, comment=comment,                                    trusted_signers=trusted_signers)    #override the to_xml() function    def to_xml(self):        s = '<?xml version="1.0" encoding="UTF-8"?>\n'        s += '<StreamingDistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2010-07-15/">\n'        s += '  <S3Origin>\n'        s += '    <DNSName>%s</DNSName>\n' % self.origin        if self.origin_access_identity:            val = self.origin_access_identity            s += '    <OriginAccessIdentity>origin-access-identity/cloudfront/%s</OriginAccessIdentity>\n' % val        s += '  </S3Origin>\n'        s += '  <CallerReference>%s</CallerReference>\n' % self.caller_reference        for cname in self.cnames:            s += '  <CNAME>%s</CNAME>\n' % cname        if self.comment:            s += '  <Comment>%s</Comment>\n' % self.comment        s += '  <Enabled>'        if self.enabled:            s += 'true'        else:            s += 'false'        s += '</Enabled>\n'        if self.trusted_signers:            s += '<TrustedSigners>\n'            for signer in self.trusted_signers:                if signer == 'Self':                    s += '  <Self/>\n'                else:                    s += '  <AwsAccountNumber>%s</AwsAccountNumber>\n' % signer            s += '</TrustedSigners>\n'        if self.logging:            s += '<Logging>\n'            s += '  <Bucket>%s</Bucket>\n' % self.logging.bucket            s += '  <Prefix>%s</Prefix>\n' % self.logging.prefix            s += '</Logging>\n'        s += '</StreamingDistributionConfig>\n'        return s    def create(self):        response = self.connection.make_request('POST',            '/%s/%s' % ("2010-11-01", "streaming-distribution"),            {'Content-Type' : 'text/xml'},            data=self.to_xml())        body = response.read()        if response.status == 201:            return body        else:            raise CloudFrontServerError(response.status, response.reason, body)cf = boto.connect_cloudfront()s3_dns_name = "stream.example.com.s3.amazonaws.com"comment = "example streaming distribution"oai = "<OAI ID from step 2 above like E23KRHS6GDUF5L>"#Create a distribution that does NOT need signed URLShsd = HackedStreamingDistributionConfig(connection=cf, origin=s3_dns_name, comment=comment, enabled=True)hsd.origin_access_identity = oaibasic_dist = hsd.create()print("Distribution with basic URLs: %s" % get_domain_from_xml(basic_dist))#Create a distribution that DOES need signed URLShsd = HackedStreamingDistributionConfig(connection=cf, origin=s3_dns_name, comment=comment, enabled=True)hsd.origin_access_identity = oai#Add some required signers (Self means your own account)hsd.trusted_signers = ['Self']signed_dist = hsd.create()print("Distribution with signed URLs: %s" % get_domain_from_xml(signed_dist))

5 - Test that you can download objects from cloudfront but not from s3

You should now be able to verify:

  • stream.example.com.s3.amazonaws.com/video.mp4 - should give AccessDenied
  • signed_distribution.cloudfront.net/video.mp4 - should give MissingKey (because the URL is not signed)
  • basic_distribution.cloudfront.net/video.mp4 - should work fine

The tests will have to be adjusted to work with your stream player, but the basic idea is that only the basic cloudfront url should work.

6 - Create a keypair for CloudFront

I think the only way to do this is through Amazon's web site. Go into your AWS "Account" page and click on the "Security Credentials" link. Click on the "Key Pairs" tab then click "Create a New Key Pair". This will generate a new key pair for you and automatically download a private key file (pk-xxxxxxxxx.pem). Keep the key file safe and private. Also note down the "Key Pair ID" from amazon as we will need it in the next step.

7 - Generate some URLs in Python

As of boto version 2.0 there does not seem to be any support for generating signed CloudFront URLs. Python does not include RSA encryption routines in the standard library so we will have to use an additional library. I've used M2Crypto in this example.

For a non-streaming distribution, you must use the full cloudfront URL as the resource, however for streaming we only use the object name of the video file. See the code below for a full example of generating a URL which only lasts for 5 minutes.

This code is based loosely on the PHP example code provided by Amazon in the CloudFront documentation.

from M2Crypto import EVPimport base64import timedef 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_base64def 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(str(message))    signature = key.sign_final()    return signaturedef 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_urldef 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}    #now base64 encode it (must be URL safe)    encoded_policy = aws_url_base64_encode(canned_policy)    #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_urldef encode_query_param(resource):    enc = resource    enc = enc.replace('?', '%3F')    enc = enc.replace('=', '%3D')    enc = enc.replace('&', '%26')    return enc#Set parameters for URLkey_pair_id = "APKAIAZCZRKVIO4BQ" #from the AWS accounts pagepriv_key_file = "cloudfront-pk.pem" #your private keypair fileresource = 'video.mp4' #your resource (just object name for streaming videos)expires = int(time.time()) + 300 #5 min#Create the signed URLpriv_key_string = open(priv_key_file).read()signed_url = get_canned_policy_url(resource, priv_key_string, key_pair_id, expires)#Flash player doesn't like query params so encode themenc_url = encode_query_param(signed_url)print(enc_url)

8 - Try out the URLs

Hopefully you should now have a working URL which looks something like this:

video.mp4%3FExpires%3D1309979985%26Signature%3DMUNF7pw1689FhMeSN6JzQmWNVxcaIE9mk1x~KOudJky7anTuX0oAgL~1GW-ON6Zh5NFLBoocX3fUhmC9FusAHtJUzWyJVZLzYT9iLyoyfWMsm2ylCDBqpy5IynFbi8CUajd~CjYdxZBWpxTsPO3yIFNJI~R2AFpWx8qp3fs38Yw_%26Key-Pair-Id%3DAPKAIAZRKVIO4BQ

Put this into your js and you should have something which looks like this (from the PHP example in Amazon's CloudFront documentation):

var so_canned = new SWFObject('http://location.domname.com/~jvngkhow/player.swf','mpl','640','360','9');    so_canned.addParam('allowfullscreen','true');    so_canned.addParam('allowscriptaccess','always');    so_canned.addParam('wmode','opaque');    so_canned.addVariable('file','video.mp4%3FExpires%3D1309979985%26Signature%3DMUNF7pw1689FhMeSN6JzQmWNVxcaIE9mk1x~KOudJky7anTuX0oAgL~1GW-ON6Zh5NFLBoocX3fUhmC9FusAHtJUzWyJVZLzYT9iLyoyfWMsm2ylCDBqpy5IynFbi8CUajd~CjYdxZBWpxTsPO3yIFNJI~R2AFpWx8qp3fs38Yw_%26Key-Pair-Id%3DAPKAIAZRKVIO4BQ');    so_canned.addVariable('streamer','rtmp://s3nzpoyjpct.cloudfront.net/cfx/st');    so_canned.write('canned');

Summary

As you can see, not very easy! boto v2 will help a lot setting up the distribution. I will find out if it's possible to get some URL generation code in there as well to improve this great library!


In Python, what's the easiest way of generating an expiring URL for a file. I have boto installed but I don't see how to get a file from a streaming distribution.

You can generate a expiring signed-URL for the resource. Boto3 documentation has a nice example solution for that:

import datetimefrom cryptography.hazmat.backends import default_backendfrom cryptography.hazmat.primitives import hashesfrom cryptography.hazmat.primitives import serializationfrom cryptography.hazmat.primitives.asymmetric import paddingfrom botocore.signers import CloudFrontSignerdef rsa_signer(message):    with open('path/to/key.pem', 'rb') as key_file:        private_key = serialization.load_pem_private_key(            key_file.read(),             password=None,            backend=default_backend()        )    signer = private_key.signer(padding.PKCS1v15(), hashes.SHA1())    signer.update(message)    return signer.finalize()key_id = 'AKIAIOSFODNN7EXAMPLE'url = 'http://d2949o5mkkp72v.cloudfront.net/hello.txt'expire_date = datetime.datetime(2017, 1, 1)cloudfront_signer = CloudFrontSigner(key_id, rsa_signer)# Create a signed url that will be valid until the specfic expiry date# provided using a canned policy.signed_url = cloudfront_signer.generate_presigned_url(    url, date_less_than=expire_date)print(signed_url)