DEMO/Infrastructure

← Back to dashboard

Cloud Functions

Audience: operator · Profile: demo · Read this when: you need a stable HTTPS endpoint external systems can call to act inside your network.

Enable it in Settings → Integrations → Cloud Functions (cloud_functions_enabled), then configure a package store in the same panel. The Functions page is at /functions. Design notes: docs/design/cloud-functions.md.

What is it?

One handler, three clouds. You write (or pick) a workload — a single Python function — and the dashboard deploys it as an AWS Lambda, an Azure Linux Function App, or a GCP Cloud Run function, with the same behaviour on each. Every function gets a stable HTTPS endpoint protected by a shared bearer secret, and can optionally be attached to a VPC/VNet so it can reach private resources.

The handler source lives in the repo at web_dashboard/functions/ and ships inside the dashboard image, so there is no build pipeline to run and no artifact to publish yourself.

Use cases

Diagnose serverless networking echo_diag reports what the function can actually reach — resolving DNS and TCP separately, so "broken" becomes "DNS" or "security group"
Certificate expiry watch cred_expiry_watch runs a TLS handshake sweep from inside the VPC, so it sees internal-only endpoints
An endpoint for external systems Something outside your network needs to trigger an action inside it, synchronously — the case a one-shot container cannot serve
Entitle REST integrations (Phase 2) Entitle POSTs Give/Revoke Access to the function, which performs the grant against a private target

The dashboard already reaches private resources for its own work by running one-shot containers inside the target cloud (ECS / ACI / Cloud Run jobs). Reach is not what this adds — an always-on, externally callable endpoint is.

Prerequisites

Cloud Needs
AWS An S3 bucket for packages. The dashboard's IAM identity needs s3:PutObject, plus lambda:*, iam:CreateRole/AttachRolePolicy/PutRolePolicy (the execution role and its inline secrets policy), secretsmanager:CreateSecret/PutSecretValue/DeleteSecret/DescribeSecret/TagResource (every function keeps its bearer secret there) and logs:* on the function's log group (including logs:DeleteLogGroup, or destroy leaves it behind). Two that are easy to miss: iam:PassRole on the execution role (lambda:CreateFunction passes it to Lambda) and lambda:AddPermission — a function URL with authorization_type = NONE needs a resource policy that CreateFunctionUrlConfig does not write, so without it every public invoke returns {"Message":"Forbidden"}. The module creates that permission itself; the deploying identity just needs to be allowed to.
Azure The dashboard's existing storage account (storage_azure_account) and resource group, plus a Key Vault — every function keeps its bearer secret there. The service principal needs Microsoft.Web/*, Microsoft.Storage/storageAccounts/listKeys/action, Microsoft.Web/sites/host/listKeys/action, Microsoft.ManagedIdentity/userAssignedIdentities/*, secret set on the vault, and authority to grant on it (Microsoft.Authorization/roleAssignments/write for an RBAC vault, or Microsoft.KeyVault/vaults/accessPolicies/write for a policy-based one).
GCP A GCS bucket for sources, the cloudfunctions + artifactregistry APIs enabled, and a noticeably larger IAM surface: roles/cloudfunctions.developer, roles/run.admin, roles/cloudbuild.builds.builder, roles/artifactregistry.writer, roles/storage.objectAdmin (project-wide, or bucket-scoped on the source bucket), roles/secretmanager.admin, plus roles/iam.serviceAccountUser on the runtime service account. Direct VPC egress itself needs no extra grant — those permissions belong to the Cloud Run service agent, which already holds them for a same-project VPC.
All Terraform in the image (it is, by default). GCP additionally needs the google provider >= 7.21, which the image pre-caches.

A missing GCP role surfaces as a 403 roughly 90 seconds into the build, after the plan succeeded. If a GCP deploy fails late, check the roles above first.

Setup

  1. Settings → Integrations → turn on Cloud Functions. The nav entry and /api/functions appear immediately; no restart.
  2. Settings → Integrations → Cloud Functions (Configure) → set the package store for each cloud you intend to use. A cloud without one is shown in the deploy form but blocked, with the reason.
  3. (Optional) Fill in the VPC / VNet attachment section if you want functions that reach private resources. See Networking.
  4. Go to Functions → Deploy function, pick echo_diag, and deploy. Then use Test invoke to confirm it works.

The adapter workloads (db_grant, portainer_access, azure_role_grant) read their target out of the function's environment and do nothing without it — the deploy form names what each one needs and refuses a deploy that omits it, because the alternative is a function that builds successfully and then fails on every request. db_grant in particular should always be deployed for you — by the Function (DB grant) button on a database's row, or at provision time: see Cloud databases and the clouddb_adapter_pair job. POST /check_config is the route to ask an already-deployed adapter whether it is configured; it answers {"data": {"valid": false, "problems": [...]}} rather than failing when it is not. Once db_grant is armed (FN_DB_DRY_RUN=0) it also opens one real connection, so it answers the question a dry run cannot: whether the function can actually reach the database.

Field reference

Field Notes
function_package_s3_bucket AWS package bucket
function_package_gcs_bucket GCP source bucket
function_package_azure_container Blob container on the dashboard's storage account (default function-packages)
azure_functions_plan_sku App Service plan SKU, default B1
aws_functions_subnet_ids / aws_functions_security_group_ids Comma-separated; used when a function is deployed in vpc mode
azure_functions_subnet_id Must be delegated to Microsoft.Web/serverFarms
gcp_functions_network / gcp_functions_subnetwork GCP Direct VPC egress — no connector, nothing billed while idle. Give the subnet as a bare name: direct egress is region-locked, and a bare name resolves in whichever region the function lands in.
gcp_functions_service_account Runtime service account. Blank falls back to the broad default compute SA — and skips the per-secret accessor binding, so the function cannot read its own bearer secret.
gcp_functions_vpc_connector Legacy fallback: an existing Serverless VPC Access connector. Only needed to reach another region from a region-pinned function. Ignored when the two keys above are set.

What it enables in the dashboard

Feature Where
Deploy / list / destroy functions Functions page, /api/functions
Workload catalog GET /api/functions/workloads
Test invoke with credentials attached Test invoke button, POST /api/functions/{id}/invoke (takes method + path, so an adapter's own routes are reachable)
Endpoint + secret for an external caller Endpoint button (admin only), GET /api/functions/{id}/invoke-info
Progress + logs for a deploy Jobs page (cloudfn_deploy / cloudfn_decommission)
Count of deployed functions, and how many are callable Cloud Functions tile, Dashboard → Managed Services
Every function alongside the rest of the estate Inventory page, kind function (GET /api/inventory?kind=function)

Workloads

Each is one module in web_dashboard/functions/fnworkloads/. The filesystem is the catalog — adding a module makes it deployable, no registry to edit. Sample request payloads for each, and a template for writing your own, are in examples/functions/.

Workload What it does
echo_diag Echoes the request as the function saw it (redacted), reports where it landed, and DNS/TCP-probes any {host, port} you pass
entitle_webhook_echo A no-op Entitle Remote Adapter — serves every route with valid empty responses and reports what arrived, without granting anything. Supports ?fail=401\|403\|404\|500\|slow\|timeout
db_grant Entitle Remote Adapter for ephemeral just-in-time database accounts on MySQL and SQL Server. Dry run by default. See below.
portainer_access Entitle Remote Adapter for just-in-time Portainer access, granted through team membership. Dry run by default. See below.
azure_role_grant Entitle Remote Adapter for time-boxed Azure RBAC on a machine identity. Dry run by default. See below.

Pages

Each workload, and the work around them, has a page:

Page Read it when
Wiring an Entitle REST integration you are pointing an Entitle custom REST integration at a function the dashboard deployed
Pairing an adapter with what it grants you are deploying a db_grant or portainer_access adapter and want it paired with its database or Portainer rather than built by hand
The db_grant workload you want Entitle to grant and revoke database roles through a function inside your network
The portainer_access workload you want Entitle to grant and revoke Portainer team membership
The azure_role_grant workload you want Entitle to grant and revoke Azure role assignments through a function
Writing your own workload none of the shipped workloads does what you need and you want to add one
How functions authenticate you are deciding how callers prove who they are to a function, and how a function reaches its cloud and the dashboard
Troubleshooting Cloud Functions a function will not deploy, answers 401 or 403, or cannot reach what it should act on

Dashboard permissions

The fourth integration is not a function at all: granting dashboard permissions happens on a dashboard-hosted endpoint, because there the dashboard is the target system and there is no function to deploy. See Entitle dashboard permissions.

Credentials

A workload that needs a credential — a database admin password, a Portainer token, an Azure client secret — gets it by reference on every cloud. You name a secret that already lives in the cloud's own secret store, and the dashboard wires up that cloud's resolution mechanism:

Cloud What you pass What happens
AWS the Secrets Manager ARN the id lands in <NAME>_SECRET_ID and the ARN on the function's role; the handler reads it at cold start
GCP the Secret Manager secret id injected as a secret_environment_variables entry, and the runtime service account is bound roles/secretmanager.secretAccessor on that secret alone
Azure the Key Vault secret name becomes an @Microsoft.KeyVault(SecretUri=…) app setting the platform resolves through the app's system-assigned identity (grant it get on the vault)

On AWS it must be the full ARN: the role's policy names ARNs, and AWS appends a random suffix to every one, so a name cannot be turned into an ARN by concatenation.

POST /api/functions
{
  "cloud": "gcp", "region": "us-central1", "name": "portainer-jit",
  "workload": "portainer_access",
  "environment": { "FN_PORTAINER_URL": "https://portainer.internal",
                   "FN_PORTAINER_DRY_RUN": "0" },
  "secret_environment": { "FN_PORTAINER_API_KEY": "portainer-jit-token" }
}

environment is for non-secret settings, and the dashboard enforces that. A credential-shaped value there is refused at deploy with a message pointing here. The reason it is an error rather than a warning: environment is stored in the deploy job's metadata, streams into the job's Live Output as Terraform renders the plan, and stays readable on the function's own console page — so by the time you saw a warning, the credential would need rotating.

Automatic pairing already works this way; nothing to configure. A db_grant adapter deployed by Register in Entitle stages the admin password in the cloud's secret store and passes only the reference.

Provenance: what is actually running

A deployed function runs privileged code from inside your network, so the question "what exactly is running in there, and where did it come from?" needs to stay answerable months later — especially once workloads come from more than one image.

Every deploy records three things on the function:

What it tells you Availability
source tree hash what code — two functions with the same value run the same handlers always
package hash the exact deployed artifact, including vendored dependencies always
git commit / ref / origin who wrote and reviewed it when the image was built with the build args

The tree hash is computed from the files that produced the package, so it cannot be wrong. Git metadata is best-effort — an image built without the build args records "unknown" rather than a guess, because a confidently wrong commit is worse than an honest blank. A build from a modified working tree is marked -dirty.

To get git provenance into your image:

docker build \
  --build-arg GIT_SHA="$(git rev-parse HEAD)" \
  --build-arg GIT_REF="$(git rev-parse --abbrev-ref HEAD)" \
  --build-arg GIT_ORIGIN="$(git config --get remote.origin.url)" \
  --build-arg BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" .

It also lands as the standard org.opencontainers.image.revision label.

Ask the function, don't trust the record. The same values are injected as FN_SOURCE_COMMIT / FN_SOURCE_TREE, and echo_diag reports them in its provenance block. So verifying a deployment is one request — and it catches the case the dashboard row cannot: a deploy that half-failed, or a function someone changed by hand.

curl -sS -X POST "$FN_URL" -H "Authorization: Bearer $SECRET" \
  -d '{"egress": false}' | jq .provenance

Networking

network_mode: "public" deploys an internet-reachable endpoint. "vpc" also attaches the function to your network.

Cloud Mechanism Watch out for
AWS vpc_config A VPC-attached Lambda has no outbound internet unless its subnets route through NAT
Azure Regional VNet integration Needs a subnet delegated to Microsoft.Web/serverFarms, and a plan that supports it — Y1 does not (plan-time failure)
GCP Direct VPC egress (direct_vpc_network_interface) Region-locked — the subnet must be in the function's region, or traffic to an internal IP silently drops. Cloud Run reserves IPs in /28 blocks and needs a /26 minimum subnet; sharing one subnet across functions is supported. Needs provider >= 7.21.
GCP (legacy) Serverless VPC Access connector Only for cross-region reach from a region-pinned function. Reference an existing one; a connector costs ~$26/mo whether invoked or not

Which subnet, in which region

Every cloud has a per-region functions network field, and it is what makes a function land on its own region's network rather than the default region's:

Cloud Per-region field(s) Flat fallback
AWS functions_subnet_ids, functions_security_group_ids aws_functions_subnet_ids, aws_functions_security_group_ids
Azure functions_subnet_id azure_functions_subnet_id
GCP functions_network, functions_subnetwork gcp_functions_network, gcp_functions_subnetwork

Set them under Settings → Multi-region (the sandbox scripts emit both halves). A field a region leaves blank falls back to its flat key, so a single-region install needs none of this and behaves exactly as before.

The fields are deliberately purpose-specific rather than reusing the generic default_subnet_id/subnetwork: a flat *_functions_* key answers two questions at once — which subnet do functions use and in the default region — and overriding it with a generic per-region subnet fixes the region by discarding the purpose, and only for non-default regions. That asymmetry (the same feature choosing a different-purpose subnet depending on whether the region happened to be the default) is the bug these fields exist to avoid.

For a caller that supplies the region programmatically rather than from a picker — the cloud-DB adapter pairing uses the database's region — the deploy is refused when that region has no per-region config set at all, rather than quietly attaching to the default region's network.

Phase 2 — Entitle REST integrations

Entitle's REST integration posts Give Access / Revoke Access to a target system and expects a synchronous response. A function is the natural receiver: it is externally reachable, responds in milliseconds, and can sit inside the VPC beside the resource being granted.

Use entitle_webhook_echo to validate the whole path — network, front door, secret, timeouts, retries — before any grant logic exists. It reports exactly which fields it recognised in the payload, which is how the schema gets pinned against a real tenant.

Planned integrations: ephemeral MySQL/SQL Server database accounts, Azure machine identity, Portainer access, and dashboard user identity. See the design doc.