#- # This document is licensed under the same conditions # as the libvmod-hoailona project. See LICENSE for details. # # Author: Geoffrey Simmons # $Module hoailona 3 "Akamai SecureHD Token Authorization VMOD" :: new OBJECT = hoailona.policy(ENUM type [, DURATION ttl] [, STRING description] [, BLOB secret] [, INT start_offset]) new OBJECT = hoailona.hosts() .add(STRING host, STRING policy [, STRING path] [, STRING description]) INT .policy(STRING host, STRING path) STRING .token([STRING acl] [, DURATION ttl] [, STRING data]) BLOB .secret() STRING .explain() STRING hoailona.version() DESCRIPTION =========== This Varnish Module (VMOD) supports use of the SecureHD Policy service provided by Akamai Media Services. Applications of the VMOD include: * Defining policies for access to media content: * Policy type TOKEN: token authorization required, with a TTL (time-to-live) limiting the duration of authorized access, and possibly with a shared secret used for keyed-hash message authentication codes (HMACs) that are required for authorization * Policy type OPEN: access permitted without authorization * Policy type DENY: access denied * Assigning policies to hosts, either globally for a host, or for sets of paths defined for the host * Determining which policy holds for a given host and path * Generating authorization tokens This manual presupposes familiarity with the Akamai SecureHD service. For more information, see the documentation provided by Akamai (see `Akamai documentation`_). The VMOD does not provide cryptographic code to generate HMACs, but it does provide the means to associate shared secrets with a policy, which can be used together with a VMOD that does compute HMACs (such as the ``blobdigest`` VMOD, see `SEE ALSO`_). The name of the VMOD is inspired by the Hawaiian word *ho`ailona*, for "sign" or "symbol" (pronounced "ho-eye-lona"), which we believe to be a suitable translation for "token". We welcome feedback from speakers of Hawaiian about the choice of the name. Defining policies ----------------- Policies are defined by means of ``policy`` objects that are constructed in ``vcl_init``. A policy is defined by its type (TOKEN, OPEN or DENY), a TTL for the TOKEN type, and possibly a shared secret used for authorization. For example:: import hoailona; import blob; sub vcl_init { # Define a policy for token authorization lasting one hour, # and associate it with a shared secret. new token_policy = hoailona.policy(TOKEN, 1h, blob.decode(encoded="secret")); # Define a policy for open access (authorization not required) new open_policy = hoailona.policy(OPEN); # Define an "access denied" policy new deny_policy = hoailona.policy(DENY); } Policy objects have no methods; they become useful when they are assigned to hosts and paths, as shown in the following. Assigning policies to hosts and paths ------------------------------------- Most of the work of the VMOD is done through the ``hosts`` object, which is used in ``vcl_init`` to assign policies to hostnames, either globally for a host, or for sets of paths on a host. Patterns for paths are defined with the same syntax used by Akamai's SecureHD Policy Editor:: sub vcl_init { # After policies have been defined as shown above ... new config = hoailona.hosts(); # Assign the token_policy globally to host example.com config.add("example.com", "token_policy"); # Assign the open_policy to the path /foo/bar on host example.org config.add("example.org", "open_policy", "/foo/bar"); # Assign the deny_policy to any path beginning with /baz/quux # on subdomains of example.org config.add("*.example.org", "deny_policy", "/baz/quux/..."); } Policies are assigned by using strings that must exactly match the object names (the symbols in the VCL source) for policy objects that were previously defined in ``vcl_init``. Details about permissible host names and the pattern syntax used for paths are given below. Determining the policy for a host and path ------------------------------------------ After policies and their assignments to hosts and paths have been configured in ``vcl_init``, the policy that holds for a given host and path can be determined from the ``policy`` method of the ``hosts`` object:: sub vcl_recv { # The policy method returns 0 for policy type DENY if (config.policy(req.http.Host, req.url) == 0) { # Handle "access denied" by returning 403 Forbidden return(synth(403)); } # .policy() returns 1 for policy type OPEN if (config.policy() == 1) { return(pass); } # .policy() returns 2 for policy type TOKEN if (config.policy() == 2) { # Handle token authorization ... # [...] } } Generating authorization tokens ------------------------------- When the policy type TOKEN has been determined for a host and path (return value 2 from the ``.policy()`` method), the ``.token()`` method can be used to generate the non-cryptographic portion of the authorization token, and ``.secret()`` can be used to retrieve the shared secret associated with the policy, to generate the HMAC for the token:: import blobdigest; import blob; sub vcl_recv { # .policy() returns 2 for policy type TOKEN if (config.policy(req.http.Host, req.url) == 2) { # Handle token authorization: # Assign the non-cryptographic part of the token to a temp # header set req.http.Tmp-Token = config.token(); # Use VMOD blobdigest to generate the HMAC, and VMOD blob # to encode the result in lower case hex. # The shared secret serves as the HMAC key, and the token just # assigned to the temp header is the message to be hashed. set req.http.Tmp-HMAC = blob.encode(HEX, LOWER, blobdigest.hmacf(SHA256, config.secret(), blob.decode(encoded= req.http.Tmp-Token))); # These two temp headers can now be combined to form the full # token string required for authorization at the Akamai # server, such as: # # "hdnea=" + req.http.Tmp-Token + "~hmac=" + req.http.Tmp-HMAC } } At minimum, the string returned by ``.token()`` contains the parameters ``st`` and ``exp``, whose values are the start and end times (epoch times) for the duration of the authorization. By default, the authorization begins "now" and lasts for the duration of the TTL defined in the policy constructor, but these can be overriden by optional parameters. Other optional parameters can provide values for additional token parameters such as ``acl`` and ``data``, as described below. The ``.secret()`` method returns the BLOB that was provided in the constructor of the policy object, for the policy that was determined for the given host and path. Together with VMODs for cryptography, this can be used to generate the HMAC for authorization. The HMAC and the string returned from ``.token()`` can then be combined to form the URL query string or Cookie as required according to Akamai's APIs (for example, by generating a redirect response from VCL). Invocations of the ``.token()`` and ``.secret()`` methods have task scope, meaning that they refer back to the most recent invocation of ``.policy()`` in the same client or backend transaction. For example, when ``.policy()`` is called in any of the ``vcl_backend_*`` subroutines, subsequent calls to ``.token()`` and ``.secret()`` in the same backend transaction are based on the policy that was determined by that call. $Object policy(PRIV_TASK, ENUM {OPEN, DENY, TOKEN} type, DURATION ttl=0, STRING description=0, BLOB secret=0, INT start_offset=0) Create a policy. The ``type`` enum is required, to classify the policy as ``OPEN``, ``DENY`` or ``TOKEN``. When ``TOKEN`` is specified, then a ``ttl`` greater than 0 MUST be specified; the TTL has no effect for the ``OPEN`` and ``DENY`` types and may be left out. The TTL determines the length of time for which token authorization is valid by default. Unless the TTL is overriden, strings generated by the ``hosts.token()`` method contain parameters (epoch times) that define the duration of the authorization to correspond with ``ttl``. The optional ``secret`` parameter may contain a shared secret for authorization, which serves as the key for an HMAC. The data type for ``secret`` is BLOB, which cannot be expressed in native VCL, but can be generated by a VMOD (such as VMOD ``blob``). By default, no shared secret is stored for the policy. The optional ``description`` parameter may contain any string; if present, this string is used in the output of the ``hosts.explain()`` method to describe the policy chosed by ``hosts.policy()``. By default, no description is stored for a policy. The optional ``start_offset`` parameter can be used to alter the "start" time (parameter ``st``) in tokens that are generated based on this policy. This can be useful, for example, to address issues of time synchronization between the Akamai server and the host on which Varnish is running. By default, ``start_offset`` is 0; in this case ``st`` is unmodified and is set to the epoch time for "now". When ``start_offset`` is set to -10, for example, then ``st`` is set to 10 seconds before "now" (and hence authorization may be less likely to fail due to unsynchronized clocks). Examples:: # Open policy, no authorization required new open = hoailona.policy(OPEN); # Token authorization required, where authorization lasts 2 hours, # using the given shared secret, and setting the start offset to # 10 seconds before "now". # (Note that in Varnish 5.0.0, the negative integer for start_offset # must be written as 0-10, because negative literals are not parsed # correctly.) import blob; new token = hoailona.policy(type=TOKEN, ttl=2h, start_offset=0-10, secret=blob.decode(decoding=HEX, encoded= "717569636B2062726F776E20666F7879")); # A policy for "access denied" new forbid = hoailona.policy(DENY, description="access denied"); $Object hosts() Create a ``hosts`` object, which provides a store for a configuration that associates with policies with hostnames, and optionally with path patterns. The constructor has no parameters; the object only becomes useful by calling the ``.add()`` method. $Method VOID .add(PRIV_TASK, STRING host, STRING policy, STRING path=0, STRING description=0) Associate ``policy`` with the ``host``, optionally restricted to the path pattern described by ``path``. The ``host`` and ``policy`` parameters are required, and must be non-empty. The ``.add()`` method MUST be called in ``vcl_init`` only. If it is called in any other subroutine, then an error message is emitted to the Varnish log (using the ``VCL_Error`` tag), and the method call is ignored. The value of ``host`` MUST be a valid host name, optionally beginning with an asterisk (``*``): * A host name may only contain alphanumeric characters, ``-`` or ``.``, and optionally the leading ``*``. * It may not begin with ``.`` or ``-``. * ``*`` may only appear as the first character. If ``host`` begins with ``*``, then host names given in the ``.policy()`` method will match any non-empty string at the beginning, if followed by the same suffix. Thus for example ``*.example.com`` can be used to specify any subdomain of ``example.com``. Host names are case-insensitive; that is, ``.policy()`` will match a host name successfully regardless of case. Policies must be added in the order for which the host name lookup is to be carried out later. This is particularly important when the asterisk is used and more specific entries exist. The string in ``policy`` MUST be identical to the object name (VCL symbol) for a ``policy`` object previously defined in ``vcl_init``. If no such policy object exists, then the VCL load will fail with an error message. If no ``path`` parameter is provided, then ``policy`` is assigned globally to the host; that is, the ``.policy()`` method will determine that the given policy holds for the host regardless of the path. In that case, subsequent invocations of ``.add()`` MAY NOT assign any other policy to the same host name, either globally or restricted to any path pattern. If the same value of ``host`` appears again in any other call to ``.add()``, then the VCL load will fail with an error message. If a ``path`` parameter is provided, then the policy holds for ``host`` for patterns that match ``path``. The VMOD uses the same pattern language for path patterns that is used by the Akamai SecureHD Policy Editor: * An asterisk matches a single path component, i.e. any non-empty string of characters that are not ``/``. * Thus ``/foo/*/bar`` matches ``/foo/baz/bar`` but not ``/foo/baz/quux/bar`` or ``/foo//bar``. * When three dots (``...``) follow or precede a slash, they match a non-empty sequence of path components of any length. * Thus ``/foo/.../bar`` matches both ``/foo/baz/bar`` and ``/foo/baz/quux/bar``, but not ``/foo//bar``. * ``/foo/bar/...`` matches any path prefixed by ``/foo/bar/``, but not ``/foo/bar/`` (with no suffix) or ``/foo/bar``. * ``.../foo/bar`` matches any path ending in ``/foo/bar``, but not ``/foo/bar`` with no prefix. * Any other dot in the pattern matches a literal dot in a path. * Any other characters in the pattern match exactly with a path. Thus if none of ``*`` or ``...`` appear in ``path``, then ``path`` specifies an exact string match. The VMOD also enforces the same syntactic restrictions on path patterns as Akamai: * Valid characters are alphanumerics, space, or any of these characters: ``_-~.%:/[]@!$&()*+,;=`` * ``...`` may only appear before or after ``/``. * Two or more consecutive asterisks are not allowed. If ``path`` violates any of these restrictions, then the VCL load will fail with an error message. Note that, while hosts are matched in order, paths are not. See :ref:`func_hosts.policy` for details. If a policy is assigned to a ``host`` and a ``path`` pattern, then subsequent invocations of ``.add()`` may assign policies to the same host and different patterns. A later invocation of ``.add()`` MAY NOT assign a policy to the same host globally, or to the same host and the same pattern. In either of these cases, the VCL load will fail with an error message. The optional ``description`` parameter may be any string, and if present it is used in the output of ``.explain()``. By default, no description is set. Examples:: sub vcl_init { new p1 = hoailona.policy(OPEN); new p2 = hoailona.policy(TOKEN, 1h); new p3 = hoailona.policy(TOKEN, 2h); new p4 = hoailona.policy(TOKEN, 3h); new deny = hoailona.policy(DENY); new h = hoailona.hosts(); # Assign a policy globally to example.com h.add("example.com", "p1"); # Assign a policy to a fixed path on subdomains of example.com h.add("*.example.com", "p2", "/foo/bar"); # Assign a policy to any path beginning with /baz/quux/ # on example.org h.add("example.org", "p3", "/baz/quux/..."); # Assign a policy to any path with three components, where # the first component is /foo/ and the last is /bar, on example.org h.add("example.org", "p4", "/foo/*/bar"); # Deny access to any path on evil.org, with a description to be used # by h.explain() h.add("evil.org", "deny", description="no access to evil.org"); } $Method INT .policy(PRIV_TASK, STRING host=0, STRING path=0) Determine the policy type that holds for ``host`` and ``path``. The return values are: * 0 for ``DENY`` * 1 for ``OPEN`` * 2 for ``TOKEN`` * -1 if no matching policy can be found * -2 if there was an internal error This method MAY NOT be called in ``vcl_init``. If it is, then the VCL load fails. The method searches for host names added by the ``.add()`` method that match ``host`` in order of addition, possibly matching the suffix if the host name in ``.add()`` began with an asterisk. If it finds a host for which a policy was assigned globally (without a path pattern), then it returns the type for that policy. If it finds a host for which path patterns were defined, it attempts to match ``path``, respecting the use of ``*`` or ``...`` in the patterns. It returns the policy type assigned for a matching pattern. If more than one path pattern was assigned for the host, then it attempts to match the "most specific" patterns first. The general idea is: if, for example, the patterns ``/foo/.../bar`` and ``/foo/.../baz/bar`` were assigned for a matching host, and the ``path`` to be matched is ``/foo/quux/baz/bar``, then the more specific pattern ``/foo/.../baz/bar`` will be matched and the policy type assigned for that pattern will be returned, even though ``/foo/.../bar`` would have also matched. Formally, the "more specific" relation is defined as: * Pattern A is more specific than pattern B if: * A has more slashes than B * otherwise (if A and B have the same number of slashes) if B contains ``...`` and A does not * else if A has fewer asterisks than B * else if A is longer than B * else if A precedes B lexigraphically Note that, in contrast to hosts, for paths the order in which they were added with the ``.add()`` method is irrelevant. The rules for path matching should be identical to those of the Akamai SecureHD Policy Editor. Subsequent calls to the ``.token()``, ``.secret()`` or ``.explain()`` methods refer to the most recent invocation of ``.policy()`` in the same task scope, that is in the same client or backend transaction. Likewise, if both the ``host`` and ``path`` parameters are empty, ``.policy()`` returns again the result of the most recent invocation with parameters. Calling ``.policy()`` with only one of the ``host`` and ``path`` parameters empty is an error. $Method STRING .token(PRIV_TASK, STRING acl=0, DURATION ttl=0, STRING data=0) If the previous invocation of ``.policy()`` determined policy type ``TOKEN`` (return value 2 from ``.policy()``), then return the non-cryptographic portion of an authorization token; return NULL if no matching policy could be determined. There are no required parameters. This method MAY NOT be called in ``vcl_init``; if it is, then the VCL load fails. If the previous ``.policy()`` call did not determine policy type TOKEN, or if ``.policy()`` was not called previously in the current task scope, then an error message is emitted to the Varnish log with the ``VCL_Error`` tag, and the method returns NULL. If none of the optional parameters are specified, then the method returns a string with the parameters ``st`` and ``exp`` for the start and end times (as epoch times) for the duration of the authorization. ``st`` is derived from "now", and may be modified by a ``start_offset`` defined for the chosen policy, as described above. ``exp`` is set by adding the duration of the TTL for the chosen policy to ``st``. So at minimum, ``.token()`` may generate a string like:: st=1484251854~exp=1484255454 The optional ``ttl`` parameter overrides the TTL determined from the policy; if set, then the ``exp`` parameter is computed accordingly. The optional parameters ``acl`` and ``data`` may be used to set values for parameters ``acl`` and/or ``data`` in the token string, if these are required for your SecureHD authorization. By default, neither of the ``acl`` or ``data`` parameters are included in the token string. Examples:: sub vcl_recv { if (config.policy(req.http.Host, req.url) == 2) { # This generates the simplest token with default values set req.http.Tmp = config.token(); # Override the TTL determined from the policy set req.http.Tmp = config.token(ttl=3h); # Include values for acl and data in the token string set req.http.Tmp = config.token(acl="/foo", data="user=foo"); # This last example may generate a token string like: # st=1484251854~exp=1484255454~acl=/foo~data=user=foo # The contents of the Tmp header may now be used as # needed for SecureHD authorization. } $Method BLOB .secret(PRIV_TASK) Return the shared secret stored for the policy determined by the previous invocation of ``.policy()``. Returns NULL if no such shared secret was specified, or if no matching policy could be determined. This method MAY NOT be called in ``vcl_init``; if it is, then the VCL load fails. If ``.policy()`` was not called previously in the current task scope, then an error message is emitted to the Varnish log with the ``VCL_Error`` tag, and the method returns NULL. Examples:: import blobdigest; import blob; sub vcl_recv { if (config.policy(req.http.Host, req.url) == 2) { # Generates the non-crypto part of the token set req.http.Tmp = config.token(); # Use VMOD blobdigest to generate the HMAC, where # the shared secret serves as the HMAC key. set req.http.Tmp-HMAC = blob.encode(HEX, LOWER, blobdigest.hmacf(SHA256, config.secret(), blob.decode(encoded= req.http.Tmp-Token))); # Concatenate elements of the authorization token set req.http.Token = "hdnea=" + req.http.Tmp + "~hmac=" + req.http.Tmp-HMAC; # The contents of the Tmp header may now be used as # a query string or cookie contents, as required for # authorization at the Akamai server (for example by # constructing a redirect response in VCL). } } $Method STRING .explain(PRIV_TASK) Returns a string describing the policy that was determined for a host and path by the most recent invocation of ``.policy()`` in the current task scope (client or backend context), suitable for diagnosis or logging. The returned string contains: * The name of the policy object that was determined * The hostname that matched * If applicable, the path pattern that matched If description strings were provided in the declaration of the policy and/or in the ``.add()`` method call that assigned the policy, then these are included in the string. The ``.explain()`` method MAY NOT be called in ``vcl_init``; if it is, then the VCL load fails. If ``.policy()`` was not called previously in the current task scope, then an error message is emitted to the Varnish log with the ``VCL_Error`` tag, and the method returns NULL. Example:: import std; sub vcl_recv { if (config.policy(req.http.Host, req.url) == 2) { # [...] } std.log("Policy determination: " + config.explain()); } $Function STRING version() Returns the version string for this VMOD. Example:: std.log("Using VMOD hoailona version " + hoailona.version()); REQUIREMENTS ============ This VMOD requires Varnish since version 5.2 LIMITATIONS =========== The VMOD uses Varnish workspace for the strings returned by the ``hosts.token()`` method and for task-scoped data saved when the ``hosts.policy()`` method is called. It also uses workspace during ``vcl_init`` for temporary internal data structures needed while policy and hosts configurations are constructed. If the VMOD's methods fail with the message ``out of space`` in the Varnish log (with the log tag ``VCL_Error``), or if VCL initialization fails with such a message, then you need to increase one or both of the varnishd runtime parameters ``workspace_client`` and ``workspace_backend``. The size of workspace during ``vcl_init`` is governed by ``workspace_client``. It appears to us that the Akamai documentation is not explicit about whether the ``...`` and ``*`` constructs in the path pattern syntax are required to match non-empty strings. Such a requirement would mean, for example, that ``/foo/*/bar`` and ``/foo/.../bar`` do not match ``/foo//bar``, that ``/foo/bar/...`` does not match ``/foo/bar/`` (with no suffix), and that ``.../foo/bar`` does not match ``/foo/bar`` (with no prefix). All of their examples show these constructs matching non-empty strings, and it seems to us to be the intuitive interpretation. So the VMOD explicitly enforces this requirement. INSTALLATION ============ See `INSTALL.rst `_ in the source repository. ACKNOWLEDGEMENTS ================ Development of this module was sponsored by BILD GmbH & Co. KG SEE ALSO ======== * varnishd(1) * vcl(7) * source repository: https://code.uplex.de/uplex-varnish/libvmod-hoailona * VMOD blobdigest: https://code.uplex.de/uplex-varnish/libvmod-blobdigest Akamai documentation -------------------- Technical documentation about SecureHD token authorization appears to be available only to Akamai customers who have access to the Luna Control Center. This public document gives a non-technical overview: * https://www.akamai.com/jp/ja/multimedia/documents/product-brief/securehd-media-content-security-product-brief.pdf Users of the Luna Control Center can consult: * SecureHD Policy Editor User's Guide * https://control.akamai.com/dl/customers/SPE/spe_ug.pdf * SecureHD Policy Editor online help * https://control.akamai.com/dl/SPE/index.htm * Sanctioned token generator code (since code is the best documentation) * https://control.akamai.com/dl/customers/SPE/EdgeAuth-latest.zip $Event event