helixordevelopers

AWS

This page shows how to run each deployment pattern on AWS, in your own accounts, with managed AWS services supplying what the runtime leaves to you: secrets, encryption keys, network isolation, logging and metrics.

Reference guidance AWS Marketplace listing: Planned

Reference examples, not shipped artifacts

The CloudFormation template, task definition, manifests and scripts on this page are examples for you to adapt. Helixor does not publish AWS images, templates or modules for the runtime. Every path here works the same way: install the runtime wheel into an image you build, store it in Amazon ECR, and bring your own license file. Account IDs, Regions, names and sizes are placeholders.

Reference architecture#

Workload account (prod) VPC · private subnets · no internet route ECS task (awsvpc, Fargate) app:8080 helixor-decision127.0.0.1:18734 VPC endpointsECR · S3 (gateway) · Secrets Manager · Logs · KMS Amazon ECRdecision image + pack Secrets Managerlicense (.hxlic), KMS CMK S3 pack bucketversioned, SSE-KMS CloudWatch Logsreceipts, EMF metrics→ S3 Object Lock
The decision path stays inside the task. AWS services are reached only through VPC endpoints, and only at start-up and for logs.

Choose a pattern#

PatternAWS servicesUse when
(a) In-processECS on Fargate or EC2, EKS, EC2Python services that need decisions on the hot path.
(b) Sidecar in an ECS taskECS on Fargate or EC2Callers in any language. Rule changes ship without touching the application image.
(c) Sidecar in an EKS podEKS, Secrets Store CSI driver, IRSA or EKS Pod IdentityYou run on Kubernetes.
(d) In-process in LambdaLambda (container image)Event-driven or spiky workloads that should scale to zero.
(e) Isolated VPCPrivate subnets, VPC endpointsNo internet route is allowed.
(f) Shared in-VPC serviceInternal ALB with mutual TLS, VPC Lattice, or API Gateway private integrationMany services share one set of policies.

The generic versions of these patterns are in Deployment patterns.

Build the images#

Start from the hardened sidecar image in Deployment patterns, built from the runtime wheel and pushed to ECR. For AWS, build one more layer per pack version. Your pipeline copies the approved pack from the S3 pack bucket into the image, and adds an entrypoint that turns the license secret into a file.

The pack is encrypted and opens only with its license, so baking it into a versioned image is safe. The image tag then becomes the unit you promote and roll back. The license never goes into an image.

FROM 111122223333.dkr.ecr.us-east-1.amazonaws.com/helixor-decision-base:0.2.1

# Approved pack, copied from the S3 pack bucket by the pipeline
COPY --chown=10001:10001 guard.hxpack /opt/helixor/pack/guard.hxpack
COPY --chmod=0755 aws-entrypoint.sh /opt/helixor/aws-entrypoint.sh

USER 10001
ENTRYPOINT ["/opt/helixor/aws-entrypoint.sh"]

The runtime reads the license from a file path. ECS can inject a Secrets Manager secret only as an environment variable, so the entrypoint writes the secret to an in-memory file with mode 600. It then removes the variable and hands over to the service:

#!/bin/sh
set -eu
umask 077                                   # new files are mode 600
: "${HELIXOR_LICENSE_JSON:?license secret was not injected}"
mkdir -p /dev/shm/helixor
printf '%s' "$HELIXOR_LICENSE_JSON" > /dev/shm/helixor/helixor.lic
unset HELIXOR_LICENSE_JSON
exec helixor-pack serve \
  --pack /opt/helixor/pack/guard.hxpack \
  --license /dev/shm/helixor/helixor.lic \
  --host 127.0.0.1 --port 18734
  • /dev/shm is the in-memory file system that containers get by default, and it stays writable with a read-only root file system. Confirm it is present on your platform version. If it is not, use a task volume. On Fargate, task storage is disk-backed ephemeral storage. On the EC2 launch type you can add a tmpfs mount in linuxParameters.
  • Leave torch out of decision images. The runtime imports it at start-up when it is installed, which adds start time and memory, and evaluation never uses it.
  • Build for arm64 if you run on Graviton-based instances or Fargate on ARM64. The runtime is pure Python and runs on both architectures. Benchmark each on your workload.

(a) In-process on ECS, EKS or EC2#

Install the wheel into your Python service's image, and mount the pack and license as in (b). Create one engine per worker process at start-up, and run one warm-up evaluation before the process reports healthy. Scale with tasks, pods or processes, about one vCPU each. See Performance efficiency.

from helixor_runtime import HelixorEngine

engine = HelixorEngine.load_pack("/opt/helixor/pack/guard.hxpack",       # one per worker process
                                 license_file="/dev/shm/helixor/helixor.lic")
engine.evaluate("warm-up")

def guard(text: str) -> str:
    result = engine.evaluate(text)
    if result.action.startswith("block_") or any(t.severity == "FATAL" for t in result.triggers):
        raise PermissionError(result.reason)
    return result.remedy.clean_text

Check engine.pack_id at start-up; see Reliability. (In 0.2.0, load_pack could not load sealed packs, so in-process deployments ran the built-in pack; 0.2.1 fixes that.)

(b) Sidecar decision service in an ECS task#

With awsvpc networking, the containers in one task share a network namespace, so the application reaches the sidecar on 127.0.0.1. Nothing outside the task can reach a port bound to loopback, and that bind is your access control: the runtime has no authentication. Do not add a port mapping for the sidecar.

{
  "family": "claims-api-prod",
  "requiresCompatibilities": ["FARGATE"],
  "networkMode": "awsvpc",
  "cpu": "1024",
  "memory": "2048",
  "runtimePlatform": { "operatingSystemFamily": "LINUX", "cpuArchitecture": "ARM64" },
  "executionRoleArn": "arn:aws:iam::111122223333:role/claims-api-execution",
  "taskRoleArn": "arn:aws:iam::111122223333:role/claims-api-task",
  "containerDefinitions": [
    {
      "name": "app",
      "image": "111122223333.dkr.ecr.us-east-1.amazonaws.com/claims-api:2.4.0",
      "essential": true,
      "portMappings": [{ "containerPort": 8080, "protocol": "tcp" }],
      "environment": [{ "name": "DECISION_URL", "value": "http://127.0.0.1:18734" }],
      "dependsOn": [{ "containerName": "helixor-decision", "condition": "HEALTHY" }],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/helixor/prod/decision",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "app"
        }
      }
    },
    {
      "name": "helixor-decision",
      "image": "111122223333.dkr.ecr.us-east-1.amazonaws.com/helixor-decision-claims-guard:1.4.0",
      "essential": true,
      "user": "10001",
      "readonlyRootFilesystem": true,
      "secrets": [
        {
          "name": "HELIXOR_LICENSE_JSON",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:111122223333:secret:prod/helixor/license-2026-AbCdEf"
        }
      ],
      "healthCheck": {
        "command": ["CMD", "python", "/opt/helixor/probe.py"],
        "interval": 10,
        "timeout": 3,
        "retries": 3,
        "startPeriod": 20
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/helixor/prod/decision",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "decision"
        }
      }
    }
  ]
}
  • Execution role: secretsmanager:GetSecretValue on the license secret and kms:Decrypt on its key, plus ECR pull and log writes. The decision container needs no task-role permissions.
  • Container health check: an exec probe that runs a real evaluation (probe.py from Reliability). It checks the pack ID and a real verdict; GET /v1/health shows only that the process is up and which pack it loaded.
  • Ordering: the application container depends on the sidecar being HEALTHY, so it never starts without a working decision service.
  • Deployment: enable the deployment circuit breaker with rollback. A task whose license and pack do not match fails its health check, and the deployment rolls back.

CloudFormation example#

This template creates the pieces around the Fargate sidecar service:

  • a customer managed KMS key
  • a versioned, SSE-KMS pack bucket that denies non-TLS access
  • an encrypted log group
  • least-privilege roles
  • a security group that allows HTTPS only to VPC endpoints
  • the task definition and service

Create the license secret outside the template, so the license contents never appear in a template or stack parameter. The template passes cfn-lint.

AWSTemplateFormatVersion: "2010-09-09"
Description: >-
  Reference example (not a shipped artifact): Helixor decision sidecar on
  AWS Fargate with Secrets Manager, KMS, CloudWatch Logs and an S3 pack bucket.

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, stage, prod]
  CostCenter:
    Type: String
  VpcId:
    Type: AWS::EC2::VPC::Id
  VpcCidr:
    Type: String
    Description: CIDR of the VPC that hosts the interface endpoints
  S3PrefixListId:
    Type: String
    Description: Prefix list of the S3 gateway endpoint (image layers come from S3)
  PrivateSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
  ClusterName:
    Type: String
  AppImage:
    Type: String
  DecisionImage:
    Type: String
    Description: Your decision image with the pack for this license baked in
  LicenseSecretArn:
    Type: String
    Description: Secrets Manager secret holding the .hxlic contents, created out of band
  LicenseSecretKmsKeyArn:
    Type: String
    Description: Customer managed KMS key that encrypts the license secret

Resources:
  DataKey:
    Type: AWS::KMS::Key
    Properties:
      Description: Helixor pack bucket and decision log group
      EnableKeyRotation: true
      KeyPolicy:
        Version: "2012-10-17"
        Statement:
          - Sid: AccountAdministration
            Effect: Allow
            Principal:
              AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root
            Action: kms:*
            Resource: "*"
          - Sid: CloudWatchLogs
            Effect: Allow
            Principal:
              Service: !Sub logs.${AWS::Region}.amazonaws.com
            Action:
              - kms:Encrypt*
              - kms:Decrypt*
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:Describe*
            Resource: "*"
            Condition:
              ArnLike:
                kms:EncryptionContext:aws:logs:arn: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/helixor/*
      Tags:
        - { Key: Application, Value: helixor-decision }
        - { Key: Environment, Value: !Ref Environment }
        - { Key: CostCenter, Value: !Ref CostCenter }

  PackBucket:
    Type: AWS::S3::Bucket
    Properties:
      VersioningConfiguration:
        Status: Enabled
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: aws:kms
              KMSMasterKeyID: !GetAtt DataKey.Arn
            BucketKeyEnabled: true
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      Tags:
        - { Key: Application, Value: helixor-decision }
        - { Key: Environment, Value: !Ref Environment }
        - { Key: CostCenter, Value: !Ref CostCenter }

  PackBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref PackBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: DenyInsecureTransport
            Effect: Deny
            Principal: "*"
            Action: s3:*
            Resource:
              - !GetAtt PackBucket.Arn
              - !Sub ${PackBucket.Arn}/*
            Condition:
              Bool:
                aws:SecureTransport: "false"

  DecisionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /helixor/${Environment}/decision
      RetentionInDays: 365
      KmsKeyId: !GetAtt DataKey.Arn
      Tags:
        - { Key: Application, Value: helixor-decision }
        - { Key: Environment, Value: !Ref Environment }
        - { Key: CostCenter, Value: !Ref CostCenter }

  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      Policies:
        - PolicyName: read-helixor-license
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: secretsmanager:GetSecretValue
                Resource: !Ref LicenseSecretArn
              - Effect: Allow
                Action: kms:Decrypt
                Resource: !Ref LicenseSecretKmsKeyArn

  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole

  ServiceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Decision tasks - no inbound, HTTPS to VPC endpoints only
      VpcId: !Ref VpcId
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Ref VpcCidr
          Description: Interface endpoints (ECR, Secrets Manager, CloudWatch Logs, KMS)
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          DestinationPrefixListId: !Ref S3PrefixListId
          Description: S3 gateway endpoint

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub helixor-decision-${Environment}
      RequiresCompatibilities: [FARGATE]
      NetworkMode: awsvpc
      Cpu: "1024"
      Memory: "2048"
      RuntimePlatform:
        OperatingSystemFamily: LINUX
        CpuArchitecture: ARM64
      ExecutionRoleArn: !GetAtt ExecutionRole.Arn
      TaskRoleArn: !GetAtt TaskRole.Arn
      ContainerDefinitions:
        - Name: app
          Image: !Ref AppImage
          Essential: true
          Environment:
            - { Name: DECISION_URL, Value: "http://127.0.0.1:18734" }
          DependsOn:
            - { ContainerName: helixor-decision, Condition: HEALTHY }
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref DecisionLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: app
        - Name: helixor-decision
          Image: !Ref DecisionImage
          Essential: true
          User: "10001"
          ReadonlyRootFilesystem: true
          Secrets:
            - { Name: HELIXOR_LICENSE_JSON, ValueFrom: !Ref LicenseSecretArn }
          HealthCheck:
            Command: ["CMD", "python", "/opt/helixor/probe.py"]
            Interval: 10
            Timeout: 3
            Retries: 3
            StartPeriod: 20
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref DecisionLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: decision
      Tags:
        - { Key: Application, Value: helixor-decision }
        - { Key: Environment, Value: !Ref Environment }
        - { Key: CostCenter, Value: !Ref CostCenter }

  Service:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !Ref ClusterName
      LaunchType: FARGATE
      DesiredCount: 2
      TaskDefinition: !Ref TaskDefinition
      PropagateTags: TASK_DEFINITION
      EnableECSManagedTags: true
      DeploymentConfiguration:
        MinimumHealthyPercent: 100
        MaximumPercent: 200
        DeploymentCircuitBreaker:
          Enable: true
          Rollback: true
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          Subnets: !Ref PrivateSubnetIds
          SecurityGroups:
            - !Ref ServiceSecurityGroup
      Tags:
        - { Key: Application, Value: helixor-decision }
        - { Key: Environment, Value: !Ref Environment }
        - { Key: CostCenter, Value: !Ref CostCenter }

Outputs:
  PackBucketName:
    Value: !Ref PackBucket
  DataKeyArn:
    Value: !GetAtt DataKey.Arn

(c) Sidecar in an EKS pod#

On EKS, the Secrets Store CSI driver with the AWS provider mounts the license from Secrets Manager as a file. That is exactly what the runtime needs, so no entrypoint script is required. The pod reads the secret with its own identity: IAM roles for service accounts (IRSA), or EKS Pod Identity.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: claims-api
  namespace: claims
  annotations:
    # IRSA: role allowed to read only this secret and decrypt with its KMS key
    eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/claims-api-helixor-license
---
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: helixor-license-2026
  namespace: claims
spec:
  provider: aws
  parameters:
    # With EKS Pod Identity instead of IRSA, add:  usePodIdentity: "true"
    objects: |
      - objectName: "prod/helixor/license-2026"
        objectType: "secretsmanager"
        objectAlias: "helixor.lic"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: claims-api
  namespace: claims
spec:
  replicas: 3
  selector:
    matchLabels: { app: claims-api }
  template:
    metadata:
      labels: { app: claims-api }
    spec:
      serviceAccountName: claims-api
      securityContext:
        runAsNonRoot: true
        fsGroup: 10001
      containers:
        - name: app
          image: 111122223333.dkr.ecr.us-east-1.amazonaws.com/claims-api:2.4.0
          env:
            - { name: DECISION_URL, value: "http://127.0.0.1:18734" }
        - name: helixor-decision
          image: 111122223333.dkr.ecr.us-east-1.amazonaws.com/helixor-decision-claims-guard:1.4.0
          command: ["helixor-pack", "serve",
                    "--pack", "/opt/helixor/pack/guard.hxpack",
                    "--license", "/run/secrets/helixor/helixor.lic",
                    "--host", "127.0.0.1", "--port", "18734"]
          securityContext:
            runAsUser: 10001
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities: { drop: ["ALL"] }
          volumeMounts:
            - { name: license, mountPath: /run/secrets/helixor, readOnly: true }
          readinessProbe:
            exec: { command: ["python", "/opt/helixor/probe.py"] }
            periodSeconds: 10
          livenessProbe:
            exec: { command: ["python", "/opt/helixor/probe.py"] }
            periodSeconds: 30
      volumes:
        - name: license
          csi:
            driver: secrets-store.csi.k8s.io
            readOnly: true
            volumeAttributes:
              secretProviderClass: helixor-license-2026
  • Scope the pod's IAM role to secretsmanager:GetSecretValue on this one secret, and kms:Decrypt on its key.
  • Mount the license volume only into the decision container. Check the file mode your provider version applies to mounted objects.
  • Kubernetes NetworkPolicy applies to the whole pod, and is enforced only when your cluster runs a policy engine, such as the VPC CNI with network policy enabled. A deny-all-egress policy on a sidecar pod also cuts off the application. For a decision workload with egress fully denied, use pattern (f). Security groups for pods are an alternative control at the ENI level.
  • Use exec probes for readiness, for the reason given in (b). An HTTP liveness probe on GET /v1/health is fine.

(d) In-process in AWS Lambda#

Package the function as a container image. The runtime's dependencies include numpy, cryptography, pydantic, fastapi and uvicorn, and a container image avoids the size limits of zip packages.

FROM public.ecr.aws/lambda/python:3.11
COPY helixor_runtime-0.2.1-py3-none-any.whl /tmp/
RUN pip install --no-cache-dir /tmp/helixor_runtime-0.2.1-py3-none-any.whl \
 && rm -f /tmp/*.whl
COPY handler.py ${LAMBDA_TASK_ROOT}/
CMD ["handler.handler"]

Create the engine during initialization, outside the handler, so each execution environment builds it once and reuses it for every invocation:

from helixor_runtime import HelixorEngine

ENGINE = HelixorEngine()      # once per execution environment
ENGINE.evaluate("warm-up")    # compile patterns during init, not on the first request

def handler(event, context):
    result = ENGINE.evaluate(event["text"])
    blocked = result.action.startswith("block_") or any(
        t.severity == "FATAL" for t in result.triggers)
    print({"action": result.action, "receipt_hash": result.receipt_hash})   # never the payload
    return {"blocked": blocked,
            "text": None if blocked else result.remedy.clean_text,
            "receipt_hash": result.receipt_hash}
  • Cold start: importing the runtime and its dependencies, plus the warm-up evaluation, happens on every new execution environment. Measure it. Use provisioned concurrency if your latency budget cannot absorb it.
  • Memory and CPU: Lambda allocates CPU in proportion to memory. Measure duration at a few memory sizes and choose the cheapest one that meets your latency.
  • Concurrency: each execution environment handles one invocation at a time, so one engine per environment is naturally safe.

For sealed packs, write the license to /tmp during initialization and pass that path to HelixorEngine.load_pack(). Read it with an SDK call, or through the AWS Parameters and Secrets Lambda Extension:

import os, boto3

def write_license(secret_id: str, path: str = "/tmp/helixor/helixor.lic") -> str:
    os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
    value = boto3.client("secretsmanager").get_secret_value(SecretId=secret_id)["SecretString"]
    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w") as f:
        f.write(value)
    return path

(e) Isolated VPC#

Run the workload in private subnets with no internet gateway and no NAT gateway. Everything the tasks need at start-up is reached through VPC endpoints:

EndpointTypeWhy
ECR API and ECR Docker registryInterfacePull images
Amazon S3GatewayImage layers, and pack objects if you fetch them at start-up
Secrets ManagerInterfaceThe license secret
CloudWatch LogsInterfaceContainer logs, receipts, EMF metrics
AWS KMSInterfaceDecrypting the secret and S3 objects
AWS STS, and your orchestrator's endpointsInterfaceOnly if your platform needs them, for example IRSA on EKS or ECS on the EC2 launch type
  • Attach endpoint policies that allow only your accounts' resources, and an S3 gateway endpoint policy that allows only your pack and log buckets.
  • Compile packs on a build host that can reach nothing outside the VPC: use helixor-pack compile without --remote. Commands that contact Helixor (compile --remote, catalog, download-pack) fail without a route, so bring catalog packs in through your artifact process.
  • Hosts use the Amazon Time Sync Service, which is reachable without internet access. License validity uses the local clock.

(f) Shared in-VPC decision service#

The runtime's own authentication is one shared bearer token over plain HTTP, so the AWS front door provides TLS and per-caller authentication. Choose one:

Front doorAuthenticationNotes
Internal Application Load BalancerMutual TLS in verify mode, with a trust store of your client CAsTarget type ip to the task.
VPC Lattice serviceIAM auth policy; callers sign requests with SigV4Identity-based access across VPCs and accounts.
API Gateway private REST APIIAM authorization and a resource policy; VPC link to a Network Load BalancerAdds throttling and request validation.
  • In this pattern the service must accept traffic from the front door, so bind helixor-pack serve to the task address. The service refuses a non-loopback address without a token, so inject HELIXOR_SERVICE_TOKEN from Secrets Manager and have the front door send it (or terminate authentication at a proxy in the task and keep the service on loopback). Set the task security group to allow ingress on that port only from the load balancer or Lattice security group. Without that rule, the service is open to the VPC.
  • Give the task security group no egress rules except to the VPC endpoints it needs. The runtime makes no outbound calls.
  • Load balancer health checks can only send GET. Point the target health check at GET /v1/health, which needs no token and shows that the process is up. Rely on the ECS container health check (the exec evaluation probe) to replace tasks that are up but wrong.
  • At the front door, set a maximum body size and an idle timeout, and never allow browser origins.

Cross-cutting guidance#

Pack artifacts in S3#

  • Keep one pack bucket per account. Enable versioning, SSE-KMS with a customer managed key, Block Public Access and a TLS-only bucket policy.
  • Lay objects out as packs/<pack_id>/<version>/<license>/guard.hxpack with the SHA-256 digest beside each object.
  • Promote by copying the approved object from the build account's bucket to the target account's bucket. Grant the target account read access and kms:Decrypt on the source key. Never recompile after approval. Remember that a pack opens only with the license it was compiled for.
  • Use a service control policy so that only the pipeline role can write to production pack buckets.

Receipts and audit logs#

Receipts are unkeyed fingerprints, so tamper evidence has to come from where you store them:

  1. Log

    Write one structured line per decision to CloudWatch Logs, in a KMS-encrypted log group. Use the field set from Operational excellence, never payloads or matched_items.

  2. Export

    Stream the log group with a subscription filter to Amazon Data Firehose, delivering to an S3 audit bucket.

  3. Lock

    Create the audit bucket with S3 Object Lock in compliance mode and a default retention that matches your regulation. Nobody, including the account root user, can delete or overwrite a locked object before its retention ends.

  4. Sign (optional)

    For per-record tamper evidence, sign each record with a KMS asymmetric key or an HMAC key before you write it; see Security.

KMS keys#

Use customer managed keys for the license secret, the pack bucket, the log group and the audit bucket. Grant kms:Decrypt on the license key only to the roles that start decision workloads. Enable automatic key rotation. KMS protects these artifacts at rest outside the runtime. The runtime itself has no KMS integration, and wrapping pack keys with a key you control is Planned.

Egress control#

  • Security groups with no 0.0.0.0/0 egress. Allow HTTPS only to the endpoints' security group or CIDR, and to the S3 prefix list.
  • VPC endpoints for every AWS service the task uses, so no NAT gateway is needed.
  • AWS Network Firewall, optionally, with a domain allow-list for any workload that must reach the internet. The decision service itself never needs to.
  • VPC Flow Logs on decision subnets. Any rejected outbound flow from a decision task is worth an alert.

Metrics with CloudWatch embedded metric format#

The runtime emits no metrics. Emit them from your application in CloudWatch embedded metric format (EMF). In Lambda, print EMF lines to standard output. On ECS or EKS, send them through the CloudWatch agent. Use the metric names from Operational excellence:

{
  "_aws": {
    "Timestamp": 1790000000000,
    "CloudWatchMetrics": [{
      "Namespace": "Helixor/Decisions",
      "Dimensions": [["PackId", "Action"]],
      "Metrics": [{ "Name": "Decisions", "Unit": "Count" },
                  { "Name": "CallerLatency", "Unit": "Milliseconds" }]
    }]
  },
  "PackId": "custom.claims_guard.v1",
  "Action": "redact_and_permit_contact_pii",
  "Decisions": 1,
  "CallerLatency": 0.4,
  "receipt_hash": "hx_proof_ddaca3a134e0b6e6599f3ecd"
}

Keep dimensions low-cardinality: pack ID and action, never request IDs. Receipt hashes belong in the record's properties, not in dimensions. Publish helixor_license_days_remaining from a daily EventBridge Scheduler job that runs the expiry check in Reliability. That job is a scheduled check, not a polling loop in the service.

Health checks#

ECS container health checks, EKS readiness probes and Lambda initialization should all run a real evaluation of a harmless payload and check the pack ID. Load balancers that can only GET check /v1/health for liveness, as described in (f).

Multi-account layout#

AWS Organizations Build accountcompile, golden testsECR, pack bucket Staging accountstaging licensecanary traffic Production accountproduction licenseapproved digest only
Each environment is its own account, with its own license. Approved packs and images are copied forward, never rebuilt.
  • Keep development, staging and production in separate member accounts, and keep security tooling and log archives in their own accounts.
  • Use one license per environment where your agreement allows. Each environment's license lives only in that account's Secrets Manager. Because a pack opens only with its license, the build account compiles one pack per environment license from the same reviewed commit.
  • Enforce guardrails with service control policies: no public S3, no deletion of audit buckets, and pipeline-only writes to pack buckets and ECR repositories.

Tagging and cost allocation#

Tag every resource with Application, Environment and CostCenter, and activate them as cost allocation tags. For ECS, set PropagateTags to TASK_DEFINITION and enable ECS managed tags, so task-level cost lands on the right owner. Add a PackId tag to shared decision services if you charge teams back by policy.

Next steps#