L

libvmod-hoailona

Varnish Module (VMOD) to support use of the SecureHD Policy service provided by Akamai Media Services

vmod_hoailona

Akamai SecureHD Token Authorization VMOD

Manual section: 3

SYNOPSIS

import hoailona [from "path"] ;

new OBJECT = hoailona.policy(ENUM type [, DURATION ttl]
                             [, STRING description] [, BLOB secret]
                             [, INT start_offset])

new OBJECT = hoailona.hosts()
<obj>.add(STRING host, STRING policy [, STRING path]
          [, STRING description])
INT <obj>.policy(STRING host, STRING path)
STRING <obj>.token([STRING acl] [, DURATION ttl] [, STRING data])
BLOB <obj>.secret()
STRING <obj>.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 hō`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 blobcode;

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,
                          blobcode.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 blobcode;

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 blobcode
       # 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
         = blobcode.encode(HEXLC,
             blobdigest.hmacf(SHA256, config.secret(),
                              blobcode.decode(IDENTITY,
                                              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.

CONTENTS

  • policy(PRIV_TASK, ENUM {OPEN,DENY,TOKEN}, DURATION, STRING, BLOB, INT)
  • hosts()
  • STRING version()

policy

new OBJ = 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 blobcode). 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 blobcode;
new token = hoailona.policy(type=TOKEN, ttl=2h, start_offset=0-10,
                            secret=blobcode.decode(HEX,
                                 "717569636B2062726F776E20666F7879"));

# A policy for "access denied"
new forbid = hoailona.policy(DENY, description="access denied");

hosts

new OBJ = 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.

hosts.add

VOID hosts.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 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");
}

hosts.policy

INT hosts.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.

hosts.token

STRING hosts.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.
}

hosts.secret

BLOB hosts.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 blobcode;

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
      = blobcode.encode(HEXLC,
           blobdigest.hmacf(SHA256, config.secret(),
                            blobcode.decode(IDENTITY,
                                            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).
  }
}

hosts.explain

STRING hosts.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());
}

version

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.1.0.

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

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:

Users of the Luna Control Center can consult:

COPYRIGHT

This document is licensed under the same conditions
as the libvmod-hoailona project. See LICENSE for details.

Author: Geoffrey Simmons <geoffrey.simmons@uplex.de>