What your infrastructure looks like.
BetaA connector reads one account’s settings on a schedule and keeps an inventory of what it finds: each resource, the settings that matter for security, when it first appeared, when it last changed and when it went. Connectors only read, and they read settings, never secret values. Start with your own TillDev workspace, then your AWS accounts, Google Cloud projects, Azure subscriptions, Cloudflare accounts and DigitalOcean teams. The clouds join through federation, so no cloud key is ever stored; Cloudflare and DigitalOcean, which have no federation, read with a read-only token sealed in TillSecrets. Manage connectors under Shield → Posture. Every sync judges each resource against a library of 96 checks, opens a finding for each one that fails, and notes what changed since the resource’s baseline. It sits beside TillDrill, which tests what your sites show from outside.
Connect this workspace
Under Shield → Posture, pick TillDev and connect. The first sync starts at once and usually takes seconds. From the CLI:
tilldev shield posture add till --wait # read this workspace first
tilldev shield posture # every connector and its state
tilldev shield posture show <connector-id> # recent syncs, and the identity to trust
tilldev shield posture sync <connector-id> --wait # exits 1 when the sync fails
tilldev shield posture schedule <connector-id> hourly # or daily, or off
tilldev shield posture resources --type till.forge_repo # the inventory, 100 a page
tilldev shield posture resources show <resource-id> # the settings last read
tilldev shield posture rm <connector-id> # asks first; --yes in scriptsThe TillDev connector reads these, and nothing else:
| Type | What is read |
|---|---|
till.forge_repoTillForge repository | Visibility, status, default branch, whether it is a fork or a mirror, the commit identity check, and the branch policy guarding the default branch: signed commits and their suite, linear history, required approvals, how many checks are required, force pushes, deletions and MFA to push. |
till.secrets_tokenTillSecrets service token | Its project and scope, whether it is pinned to one environment, how many keys it may read, whether it is bound to a host key or a workload identity, when it expires and the day it was last used. Revoked tokens are left out. |
till.api_keyWorkspace API key | Its scopes, the resources it is pinned to, how many addresses its allowlist holds, whether minting it took MFA, when it expires and when it was last used. |
till.auth_appTillAuth app | MFA enforcement, each sign-in method, self sign-up and the allowlist, how many redirect origins it accepts and whether any is plain http off loopback, SCIM, and whether it signs with its own key. |
How each provider is reached
| Provider | Access | Account | Available |
|---|---|---|---|
| TillDev | Built in; no credential | This workspace | Yes |
| AWS | Federated; no stored key | Account ID | Yes |
| Google Cloud | Federated; no stored key | Project ID | Yes |
| Azure | Federated; no stored key | Subscription ID | Yes |
| Cloudflare | A read-only API token, sealed in TillSecrets | Account ID | Yes |
| DigitalOcean | A read-only API token, sealed in TillSecrets | Team UUID | Yes |
For a cloud account, TillDev signs a token that lasts five minutes, naming the connector as its subject, and the account exchanges it for read-only access through its own identity federation. You trust two values in the account: the issuer, https://tilldev.dev/oidc/connectors, and the subject the connector shows, workspace:<workspace-id>:connector:<connector-id>. Pin the subject, not only the issuer, so no other workspace’s connector can use the trust. The issuer publishes its discovery document at /oidc/connectors/.well-known/openid-configuration and its public keys at /oidc/connectors/jwks.json. Remove the trust in the account and the next sync can’t read.
A provider without federation keeps a read-only token. It is sealed with your workspace’s own key in TillSecrets, unsealed only for a sync that is running, and never returned by the API: you see its last four characters. Replace it any time; disconnecting destroys it. Every time a sync is given a token or a federation token is signed for it, TillSecrets records it in the audit log.
Connect an AWS account
An AWS connector holds no AWS key. Each sync assumes a role in your account for fifteen minutes, using a token TillDev signs for that connector alone. Adding the connector starts no sync: first the account has to trust it, in three steps, and Shield → Posture shows them with your account and connector filled in.
- In IAM, add an OpenID Connect identity provider with the URL
https://tilldev.dev/oidc/connectorsand the audiencests.amazonaws.com. One provider serves every connector in the account. - Create a role,
TillDevPostureunless you name another, with the trust policy below. It names this connector’s subject, so no other workspace and no other connector can assume it. - Give the role the read policy below. It allows exactly the 21 calls a sync makes, all of them reads. The AWS managed SecurityAudit policy works too, and allows more.
tilldev shield posture add aws --account 123456789012 # --role and --regions are optional
tilldev shield posture trust <connector-id> --out . # writes trust.json and read.json
aws iam create-open-id-connect-provider --url https://tilldev.dev/oidc/connectors --client-id-list sts.amazonaws.com
aws iam create-role --role-name TillDevPosture --assume-role-policy-document file://trust.json
aws iam put-role-policy --role-name TillDevPosture --policy-name TillDevPostureRead --policy-document file://read.json
tilldev shield posture sync <connector-id> --wait{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/tilldev.dev/oidc/connectors"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"tilldev.dev/oidc/connectors:aud": "sts.amazonaws.com",
"tilldev.dev/oidc/connectors:sub": "workspace:<workspace-id>:connector:<connector-id>"
}
}
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"cloudtrail:DescribeTrails",
"cloudtrail:GetTrailStatus",
"ec2:DescribeInstances",
"ec2:DescribeRegions",
"ec2:DescribeSecurityGroups",
"ec2:GetEbsEncryptionByDefault",
"iam:GenerateCredentialReport",
"iam:GetAccountPasswordPolicy",
"iam:GetAccountSummary",
"iam:GetCredentialReport",
"iam:ListRoles",
"rds:DescribeDBInstances",
"s3:GetAccountPublicAccessBlock",
"s3:GetBucketLocation",
"s3:GetBucketLogging",
"s3:GetBucketOwnershipControls",
"s3:GetBucketPolicyStatus",
"s3:GetBucketPublicAccessBlock",
"s3:GetBucketVersioning",
"s3:GetEncryptionConfiguration",
"s3:ListAllMyBuckets"
],
"Resource": "*"
}
]
}A sync reads every region enabled for the account unless the connector lists the regions to read. A region the role can’t read, for example because a service control policy blocks it, leaves the regional types partial and names the region; list only the regions you use and the next sync reads in full. A sync makes at most 10,000 AWS calls and retries when AWS slows it down. Only the commercial AWS partition can be connected; China and GovCloud can’t yet.
| Type | What is read |
|---|---|
aws.accountAWS account | Whether the root user has MFA and active access keys and when its password was last used, how many users, roles and MFA devices the account holds, the IAM password policy, and S3 Block Public Access for the account. |
aws.iam_userIAM user | From the IAM credential report: console access, MFA, the day the password was last used and changed, each access key with whether it is active, when it was rotated and the day it was last used, and active signing certificates. |
aws.iam_roleIAM role | Its path, whether AWS manages it, its longest session, and who its trust policy lets in: services, identity providers, accounts (and how many are outside this one), and whether it trusts anyone, with or without a condition. |
aws.s3_bucketS3 bucket | Its region, Block Public Access, whether its policy makes it public, default encryption and whether that uses a KMS key, versioning and MFA delete, access logging and object ownership. |
aws.regionAWS region | Each enabled region: whether new EBS volumes are encrypted by default, and whether a logging CloudTrail trail covers it. |
aws.security_groupEC2 security group | Its VPC, every inbound rule with its protocol, ports and sources, the rules open to the internet, and whether all outbound traffic is allowed. |
aws.ec2_instanceEC2 instance | State, type, image, network, public addresses, whether IMDSv2 is required, its instance profile and security groups. Terminated instances are left out. |
aws.rds_instanceRDS instance | Engine and version, whether it is publicly accessible and its endpoint, storage encryption, IAM authentication, backup retention, deletion protection, Multi-AZ and minor version upgrades. |
aws.cloudtrail_trailCloudTrail trail | Whether it covers every region and global services, log file validation, KMS encryption, its bucket, whether it is an organization trail, and whether it is logging and delivering. |
A setting the role isn’t allowed to read is listed under unread on that resource rather than guessed. When a sync can’t start a session it fails and says why: the account doesn’t trust the issuer yet, the role is missing or its trust policy names another connector, or the audience is wrong.
Connect a Google Cloud project
A Google Cloud connector holds no service account key. Each sync exchanges a token TillDev signs for that connector at Google’s security token service, through a workload identity pool you control, and reads with an access token that lasts 15 minutes. Adding the connector starts no sync: first the project has to trust it, and Shield → Posture shows the steps with your project and connector filled in.
- Choose the pool and provider names, and add the connector with the provider’s full name, starting
//iam.googleapis.com/, as its audience. The connector then shows its subject. - Create the workload identity pool and an OpenID Connect provider in it with the issuer
https://tilldev.dev/oidc/connectors, the attribute mappinggoogle.subject=assertion.sub, the allowed audiences left empty, and the attribute conditionassertion.sub == '<subject>'. The condition names this connector, so no other workspace and no other connector can use the provider. - Grant the connector’s principal read access on the project: a custom role with the 9 permissions a sync uses, all of them reads, or the predefined Security Reviewer and Viewer roles, which allow more.
tilldev shield posture add gcp --account my-project-123 --audience //iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/tilldev/providers/tilldev
tilldev shield posture trust <connector-id> # the subject, principal and commands, filled in
gcloud iam workload-identity-pools create tilldev --project=123456 --location=global
gcloud iam workload-identity-pools providers create-oidc tilldev --project=123456 --location=global \
--workload-identity-pool=tilldev --issuer-uri=https://tilldev.dev/oidc/connectors \
--attribute-mapping=google.subject=assertion.sub --attribute-condition="assertion.sub == 'workspace:<workspace-id>:connector:<connector-id>'"
gcloud iam roles create tillDevPostureRead --project=my-project-123 --title="TillDev posture read" \
--permissions=cloudsql.instances.list,compute.firewalls.list,compute.instances.list,iam.serviceAccountKeys.list,iam.serviceAccounts.list,resourcemanager.projects.get,resourcemanager.projects.getIamPolicy,storage.buckets.getIamPolicy,storage.buckets.list
gcloud projects add-iam-policy-binding my-project-123 --role=projects/my-project-123/roles/tillDevPostureRead \
--member="principal://iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/tilldev/subject/workspace:<workspace-id>:connector:<connector-id>"
tilldev shield posture sync <connector-id> --waitTo read as a service account instead, name it on the connector, grant it the read role, and grant the connector’s principal Workload Identity User on that service account. The IAM Service Account Credentials API must be on in the project that owns it. An API that is off in the project, such as Cloud SQL, leaves its type empty and is listed on the project rather than failing the sync. A zone Google Cloud can’t reach leaves instances partial and names it. A sync makes at most 10,000 Google Cloud calls.
| Type | What is read |
|---|---|
gcp.projectGoogle Cloud project | Its number, state and parent, and from its IAM policy: roles granted to anyone on the internet, owners and editors, personal Gmail accounts, service accounts holding owner or editor, who outside the service accounts may impersonate any of them, conditional bindings, data access audit logging, and the APIs that are turned off. |
gcp.service_accountService account | Whether it is disabled, whether it is a default Compute Engine or App Engine account, how many user-managed keys are active and the day the oldest was created. |
gcp.storage_bucketCloud Storage bucket | Location and class, uniform bucket-level access, public access prevention, roles its IAM policy grants to anyone on the internet, versioning, access logging, a customer-managed key, and retention with its lock. |
gcp.firewall_ruleVPC firewall rule | Its network, direction, priority and whether it is disabled, source and destination ranges, allowed and denied protocols and ports, its targets, whether it lets the internet in, and firewall logging. |
gcp.compute_instanceCompute Engine instance | Zone, status and machine type, external addresses, its service accounts and whether one is the default with full API access, Shielded VM and Confidential VM, OS Login, the serial port, blocked project SSH keys, IP forwarding and deletion protection. |
gcp.sql_instanceCloud SQL instance | Engine and version, public IP and private network, authorized networks and whether any is the whole internet, the SSL mode, backups and point-in-time recovery, high availability, deletion protection and database flags. |
When the exchange is refused the sync fails and says why: the pool or provider doesn’t exist or is disabled, the attribute condition names another connector, the provider limits its audiences, or its issuer isn’t TillDev’s.
Connect an Azure subscription
An Azure connector holds no client secret. Each sync signs in to Microsoft Entra ID as an app registration in your tenant, using a token TillDev signs for that connector alone as the app’s federated credential, and reads with the Reader role. Adding the connector starts no sync: first the app has to trust it, and Shield → Posture shows the steps with your tenant, app and subscription filled in.
- Register an app in your tenant, or use one you have, and make sure it has a service principal. One app can serve every connector; add the connector with the tenant ID and the app’s client ID.
- Add a federated credential to the app with the issuer
https://tilldev.dev/oidc/connectors, the audienceapi://AzureADTokenExchangeand the subject the connector shows. The subject names this connector, so no other workspace can sign in as the app. - Assign the app the built-in Reader role on the subscription. Reader reads settings and never the data in storage, databases or vaults.
az login --tenant <tenant-id>
az ad app create --display-name "TillDev posture" # note its appId, the client ID
az ad sp create --id <client-id>
tilldev shield posture add azure --account 00000000-0000-0000-0000-000000000000 --tenant-id <tenant-id> --client-id <client-id>
tilldev shield posture trust <connector-id> --out . # writes credential.json
az ad app federated-credential create --id <client-id> --parameters credential.json
az role assignment create --assignee <client-id> --role Reader --scope /subscriptions/00000000-0000-0000-0000-000000000000
tilldev shield posture sync <connector-id> --wait{
"name": "tilldev-<connector-id>",
"issuer": "https://tilldev.dev/oidc/connectors",
"subject": "workspace:<workspace-id>:connector:<connector-id>",
"audiences": [
"api://AzureADTokenExchange"
],
"description": "TillDev posture connector"
}A sync makes 12 kinds of Azure Resource Manager read, each pinned to an API version, and follows the next page only on Resource Manager. It checks the subscription belongs to the connector’s tenant before reading anything else. A resource provider that isn’t registered leaves its type empty and is listed on the subscription rather than failing the sync. A sync makes at most 10,000 Azure calls and waits as long as Azure asks when it slows down. Only the global Azure cloud can be connected for now.
| Type | What is read |
|---|---|
azure.subscriptionAzure subscription | Its state and tenant, who holds Owner, Contributor and User Access Administrator directly on it and how many owners are service principals, custom roles that allow every action, each Defender for Cloud plan and the ones on the free tier, whether the activity log is exported, and resource providers that aren’t registered. |
azure.storage_accountStorage account | Kind and SKU, anonymous blob access, HTTPS only, minimum TLS, shared key access, public network access and the network default, a customer-managed key, infrastructure encryption and cross-tenant replication. |
azure.network_security_groupNetwork security group | Every inbound rule with its priority, access, protocol, sources and ports, the rules that let the internet in, and how many subnets and interfaces it guards. |
azure.virtual_machineVirtual machine | Size and OS, whether Linux allows password sign-in, public addresses, an interface with no security group, Trusted Launch with secure boot and vTPM, encryption at host, managed identity and boot diagnostics. |
azure.sql_serverAzure SQL server | Version, public network access, minimum TLS, Microsoft Entra-only authentication, firewall rules and whether one allows all of Azure or the whole internet, and auditing. |
azure.key_vaultKey vault | SKU, soft delete and its days, purge protection, RBAC or access policies, public network access and the network default. |
When sign-in is refused the sync fails and says why: no federated credential trusts the issuer or names this connector, the audience is wrong, the app or the tenant can’t be found, or the app is disabled.
Connect a Cloudflare account
Cloudflare has no identity federation for API access, so a Cloudflare connector reads with a read-only token you create. Create it as an Account API token on the account, or as a user token limited to it, at https://dash.cloudflare.com/profile/api-tokens, with only the permissions below. The token is sealed in TillSecrets as it arrives, shown only by its last four characters, and unsealed only for a sync that is running. The first sync starts as soon as the connector is added.
| Scope | Permission | What it lets a sync read |
|---|---|---|
| Account | Account Settings · Read | the account, its two-factor rule and its members |
| Account | Account API Tokens · Read | the account’s API tokens |
| Account | Workers R2 Storage · Read | R2 buckets and their public access |
| Zone | Zone · Read | the zones |
| Zone | Zone Settings · Read | each zone’s TLS and security settings |
| Zone | DNS · Read | DNSSEC and records that aren’t proxied |
printf %s "$CF_TOKEN" | tilldev shield posture add cloudflare --account <account-id> --token-stdin --wait
tilldev shield posture trust <connector-id> # the permissions, for a replacement token
printf %s "$NEW_TOKEN" | tilldev shield posture token <connector-id> --token-stdinEvery call goes to the Cloudflare API with the token and nothing else. A sync first reads the account and stops if the token answers for a different one. Leave out a permission for what you don’t use and that type reads as partial, naming the permission it needs; a zone whose settings or DNS can’t be read keeps its name and lists what wasn’t read. A token Cloudflare no longer accepts fails the sync and says to replace it. Give the token read permissions only: TillDev never writes, and a token that can’t write can’t be misused if it ever leaks.
| Type | What is read |
|---|---|
cloudflare.accountCloudflare account | Its type, whether it enforces two-factor sign-in, how many members it has and how many are invited but not yet joined, super administrators, and members without two-factor. |
cloudflare.memberAccount member | Each member by email: status, roles and permission groups, whether they are a super administrator, and two-factor. |
cloudflare.api_tokenAccount API token | The account’s own API tokens: status, permission groups, whether any can write, whether a policy covers every resource, IP limits, when it was issued, when it expires and the day it was last used. |
cloudflare.zoneZone | Status, plan and whether it is paused, SSL mode, Always Use HTTPS, minimum TLS, TLS 1.3, Automatic HTTPS Rewrites, HSTS and whether it lasts a year, security level, development mode, DNSSEC, and how many A and AAAA records aren’t proxied, with a few by name. |
cloudflare.r2_bucketR2 bucket | Location, jurisdiction and storage class, whether its public r2.dev address is on, its custom domains with their minimum TLS, and whether it is public at all. |
Connect a DigitalOcean team
DigitalOcean has no identity federation for its API either, so a DigitalOcean connector reads with a personal access token with custom scopes. Make it in the team you connect, at https://cloud.digitalocean.com/account/api/tokens, with only the read scopes below, and add the connector with the team’s UUID. The token is sealed in TillSecrets as it arrives, like a Cloudflare token, and the first sync starts at once.
| Scope | What it lets a sync read |
|---|---|
account:read | the team the token belongs to |
ssh_key:read | the SSH keys new Droplets can be given |
droplet:read | Droplets, their addresses, backups and monitoring |
firewall:read | cloud firewalls and what they let in |
database:read | managed databases and their trusted sources |
kubernetes:read | Kubernetes clusters and their upgrades |
load_balancer:read | load balancers and their forwarding rules |
printf %s "$DO_TOKEN" | tilldev shield posture add digitalocean --account <team-uuid> --token-stdin --wait
tilldev shield posture trust <connector-id> # the scopes, for a replacement tokenA sync first reads the account and stops if the token belongs to another team. A scope left out makes its type partial and names the scope; without firewall:read Droplets are still read, but which firewalls guard them is left blank rather than guessed. Spaces buckets aren’t read: the DigitalOcean API can’t list them with a token.
| Type | What is read |
|---|---|
digitalocean.teamDigitalOcean team | The team’s name and status, whether its email is verified, its Droplet limit and how many SSH keys new Droplets can be given. |
digitalocean.dropletDroplet | Status, size, image and region, public IPv4 and IPv6 addresses, backups, the monitoring agent, VPC, the cloud firewalls that guard it by id or tag, whether none do, and tags. |
digitalocean.firewallCloud firewall | Every inbound rule with its protocol, ports and addresses, the rules open to the internet, whether outbound traffic may go anywhere, and how many Droplets and tags it covers. |
digitalocean.databaseManaged database | Engine, version, status and nodes, SSL, private network, its trusted sources and whether any address can connect, which is the case when there are none. |
digitalocean.kubernetes_clusterKubernetes cluster | Version, status, auto-upgrade and surge upgrade, high availability, VPC, the control plane firewall and the addresses it allows, node pools and nodes, and the registry. |
digitalocean.load_balancerLoad balancer | Status and address, its entry protocols and ports, plain HTTP and whether it redirects to HTTPS, TLS passthrough, its firewall and how many Droplets it serves. |
What a sync changes
- A resource seen for the first time is added.
- A resource whose name, region or settings differ from the last read is marked changed.
- A resource is removed only when the sync read its whole type and didn’t find it. A type that couldn’t be read in full, because the provider refused or a cap was reached, keeps what it had, and the sync ends as
partialwith the reason. - A removed resource that appears again comes back, with its history.
A sync that fails changes nothing and says why in words, such as a role that can no longer be assumed. Removed resources stay visible under Removed for 30 days; sync history is kept for 90 days. Disconnecting a connector drops its inventory, and its audit trail stays.
Checks and findings
After each sync, every resource is judged against the checks for its type. A check that fails opens a finding that names the fields that failed, such as internet_admin_ports = 22. A finding stays open while the check fails and resolves on its own when a later full read shows it fixed, when the resource leaves the inventory, or when the check is turned off. A check that needs a field the sync couldn’t read, because the provider didn’t answer or a permission was left out, is counted as not run: it never passes and never closes a finding. Open a resource in the inventory to see how every check judges it.
tilldev shield posture findings # failing checks, worst first
tilldev shield posture finding <finding-id> # the evidence, the fix, and any exception
tilldev shield posture checks --category network # the library: level, mode and open count
tilldev shield posture check posture-aws-s3-logging off # or alert, report, default
tilldev shield posture except <check-id> --resource <resource-id> --reason "…" --days 90
tilldev shield posture exceptions # --ended adds revoked and expired ones
tilldev shield posture drift # what changed since each baseline
tilldev shield posture baseline <resource-id> # or --connector <id>The library, by category:
| Check | Level | Applies to |
|---|---|---|
| S3 Block Public Access is not fully on for the account Data exposure · posture-aws-account-public-access | high | AWS account |
| An S3 bucket still honours object ACLs Data exposure · posture-aws-s3-acls | low | S3 bucket |
| An S3 bucket does not block public access itself Data exposure · posture-aws-s3-public-access-block | medium | S3 bucket |
| An S3 bucket policy makes the bucket public Data exposure · posture-aws-s3-public | critical | S3 bucket |
| A storage account allows anonymous blob access Data exposure · posture-azure-storage-public-blob | high | Storage account |
| An R2 bucket is public Data exposure · posture-cf-r2-public | high | R2 bucket |
| A Cloud Storage bucket is public Data exposure · posture-gcp-bucket-public | critical | Cloud Storage bucket |
| A Cloud Storage bucket uses object ACLs Data exposure · posture-gcp-bucket-uniform | low | Cloud Storage bucket |
| A project grants a role to everyone Data exposure · posture-gcp-public-iam | critical | Google Cloud project |
| A repository is public Data exposure · posture-till-repo-public | informational | TillForge repository |
| The IAM password policy is missing or allows short passwords Identity · posture-aws-password-policy | medium | AWS account |
| An IAM role can be assumed by any AWS principal Identity · posture-aws-role-open-trust | critical | IAM role |
| The root user has access keys Identity · posture-aws-root-access-keys | critical | AWS account |
| The root user has no MFA Identity · posture-aws-root-mfa | critical | AWS account |
| The root user signed in within the last 30 days Identity · posture-aws-root-recent-use | medium | AWS account |
| An IAM user can sign in to the console without MFA Identity · posture-aws-user-mfa | high | IAM user |
| An IAM user has an access key older than 90 days Identity · posture-aws-user-stale-keys | medium | IAM user |
| An IAM user has credentials unused for 90 days Identity · posture-aws-user-unused-credentials | medium | IAM user |
| A storage account accepts shared key authorisation Identity · posture-azure-storage-shared-key | low | Storage account |
| A custom role allows every action Identity · posture-azure-wildcard-role | high | Azure subscription |
| The Cloudflare account does not require 2FA Identity · posture-cf-enforce-2fa | high | Cloudflare account |
| A Cloudflare member has no 2FA Identity · posture-cf-member-2fa | high | Account member |
| A Cloudflare API token can write, never expires and works from anywhere Identity · posture-cf-token-unrestricted | medium | Account API token |
| A Cloudflare API token has not been used for 90 days Identity · posture-cf-token-unused | low | Account API token |
| Personal Gmail accounts hold project roles Identity · posture-gcp-personal-accounts | medium | Google Cloud project |
| Users can impersonate every service account in the project Identity · posture-gcp-project-impersonation | medium | Google Cloud project |
| A service account key is older than 90 days Identity · posture-gcp-sa-key-age | medium | Service account |
| A service account has user-managed keys Identity · posture-gcp-sa-keys | low | Service account |
| A service account holds Owner or Editor Identity · posture-gcp-sa-primitive | high | Google Cloud project |
| A binding grants cluster-admin Identity · posture-k8s-cluster-admin-binding | high | k8s.role_binding |
| A binding grants a role to anonymous or every signed-in user Identity · posture-k8s-public-binding | critical | k8s.role_binding |
| A role grants every verb on every resource Identity · posture-k8s-wildcard-role | high | k8s.role |
| A workspace API key never expires and works from any address Identity · posture-till-api-key-open | low | Workspace API key |
| A TillAuth app has MFA turned off Identity · posture-till-auth-mfa-off | medium | TillAuth app |
| A protected default branch still allows force pushes Identity · posture-till-repo-force-push | medium | TillForge repository |
| A repository's default branch is unprotected Identity · posture-till-repo-unprotected | medium | TillForge repository |
| A TillSecrets service token never expires Identity · posture-till-secrets-token-no-expiry | low | TillSecrets service token |
| A TillSecrets service token is bound to nothing Identity · posture-till-secrets-token-unbound | medium | TillSecrets service token |
| A default security group allows inbound traffic Network · posture-aws-default-sg-rules | medium | EC2 security group |
| An RDS instance is publicly accessible Network · posture-aws-rds-public | high | RDS instance |
| An Azure SQL server allows every IPv4 address Network · posture-azure-sql-open | critical | Azure SQL server |
| A storage account accepts traffic from all networks Network · posture-azure-storage-network-open | medium | Storage account |
| A VM with a public address has a network interface without an NSG Network · posture-azure-vm-no-nsg | medium | Virtual machine |
| A zone is not signed with DNSSEC Network · posture-cf-zone-dnssec | low | Zone |
| A zone has address records that bypass the proxy Network · posture-cf-zone-unproxied | informational | Zone |
| A managed database accepts connections from any address Network · posture-do-db-open | critical | Managed database |
| A Droplet with a public address has no cloud firewall Network · posture-do-droplet-no-firewall | high | Droplet |
| A Kubernetes control plane accepts any address Network · posture-do-k8s-cp-firewall | medium | Kubernetes cluster |
| An instance forwards traffic for other addresses Network · posture-gcp-ip-forwarding | low | Compute Engine instance |
| A Cloud SQL instance accepts connections from any address Network · posture-gcp-sql-open | critical | Cloud SQL instance |
| A Service opens a load balancer to every address Network · posture-k8s-public-load-balancer | medium | k8s.service |
| A remote administration port is open to the internet Network · posture-net-admin-open | high | EC2 security group, Network security group, Cloud firewall, VPC firewall rule |
| Every port is open to the internet Network · posture-net-all-ports-open | high | EC2 security group, Network security group, Cloud firewall, VPC firewall rule |
| A database port is open to the internet Network · posture-net-db-open | high | EC2 security group, Network security group, Cloud firewall, VPC firewall rule |
| No CloudTrail trail logs an enabled region Logging · posture-aws-cloudtrail-off | high | AWS region |
| An S3 bucket has no access logging Logging · posture-aws-s3-logging | low | S3 bucket |
| A CloudTrail trail is not logging Logging · posture-aws-trail-stopped | high | CloudTrail trail |
| A CloudTrail trail does not validate its log files Logging · posture-aws-trail-validation | medium | CloudTrail trail |
| The activity log is not exported Logging · posture-azure-activity-log | medium | Azure subscription |
| An Azure SQL server has auditing off Logging · posture-azure-sql-auditing | medium | Azure SQL server |
| Data access audit logs are off for some services Logging · posture-gcp-audit-logs | medium | Google Cloud project |
| New EBS volumes are not encrypted by default Encryption · posture-aws-ebs-default-encryption | medium | AWS region |
| An RDS instance stores data unencrypted Encryption · posture-aws-rds-unencrypted | high | RDS instance |
| A CloudTrail trail is not encrypted with a KMS key Encryption · posture-aws-trail-kms | low | CloudTrail trail |
| An Azure SQL server accepts TLS older than 1.2 Encryption · posture-azure-sql-tls | medium | Azure SQL server |
| A storage account accepts plain HTTP Encryption · posture-azure-storage-https | medium | Storage account |
| A storage account accepts TLS older than 1.2 Encryption · posture-azure-storage-tls | medium | Storage account |
| A zone does not send HSTS Encryption · posture-cf-zone-hsts | low | Zone |
| A zone serves plain HTTP without redirecting Encryption · posture-cf-zone-https | medium | Zone |
| A zone accepts TLS older than 1.2 Encryption · posture-cf-zone-min-tls | medium | Zone |
| A zone sends traffic to the origin without verified TLS Encryption · posture-cf-zone-ssl | high | Zone |
| A managed database does not require TLS Encryption · posture-do-db-ssl | medium | Managed database |
| A load balancer serves plain HTTP without redirecting Encryption · posture-do-lb-plain-http | medium | Load balancer |
| A Cloud SQL instance accepts unencrypted connections Encryption · posture-gcp-sql-ssl | medium | Cloud SQL instance |
| A TillAuth app redirects to a plain HTTP address Encryption · posture-till-auth-http-redirect | high | TillAuth app |
| An EC2 instance still answers IMDSv1 Compute · posture-aws-imdsv1 | medium | EC2 instance |
| Some Microsoft Defender for Cloud plans are off Compute · posture-azure-defender-off | low | Azure subscription |
| A Linux VM allows SSH password login Compute · posture-azure-vm-password-login | medium | Virtual machine |
| A Kubernetes cluster does not upgrade patch releases Compute · posture-do-k8s-auto-upgrade | low | Kubernetes cluster |
| An instance runs as the default service account with full API access Compute · posture-gcp-default-sa-full-access | high | Compute Engine instance |
| An instance boots without Secure Boot Compute · posture-gcp-secure-boot | low | Compute Engine instance |
| An instance accepts serial console connections Compute · posture-gcp-serial-port | medium | Compute Engine instance |
| A container adds a capability that breaks isolation Compute · posture-k8s-dangerous-capabilities | high | k8s.workload |
| A workload shares the node's network, process or IPC namespace Compute · posture-k8s-host-namespaces | high | k8s.workload |
| A workload mounts a path from the node Compute · posture-k8s-host-path | high | k8s.workload |
| A container has no memory limit Compute · posture-k8s-no-memory-limit | low | k8s.workload |
| A container can gain more privileges than it started with Compute · posture-k8s-privilege-escalation | medium | k8s.workload |
| A container runs privileged Compute · posture-k8s-privileged | high | k8s.workload |
| A container may run as root Compute · posture-k8s-run-as-root | medium | k8s.workload |
| A container image is not pinned to a version Compute · posture-k8s-unpinned-image | low | k8s.workload |
| An RDS instance keeps under 7 days of backups Backup · posture-aws-rds-backups | medium | RDS instance |
| An RDS instance can be deleted without a safeguard Backup · posture-aws-rds-deletion-protection | low | RDS instance |
| An S3 bucket keeps no previous versions Backup · posture-aws-s3-versioning | low | S3 bucket |
| A key vault has purge protection off Backup · posture-azure-vault-purge | medium | Key vault |
| A Droplet has backups off Backup · posture-do-droplet-backups | low | Droplet |
| A Cloud SQL instance has automated backups off Backup · posture-gcp-sql-backups | medium | Cloud SQL instance |
Modes and exceptions
Each check has a mode for the workspace:
- Alert opens findings, and a sync that finds new failures sends one message naming them over the workspace’s security notifications: email, Slack or a signed webhook. Critical and high checks alert by default.
- Report opens findings without alerting. Medium, low and informational checks report by default.
- Off doesn’t evaluate the check, and its open findings resolve on the next sync.
An exception accepts a risk you have decided to keep: for one resource, or for every resource a check applies to. It needs a reason of up to 500 characters and ends after 1 to 365 days. While it is in force the check still runs, but its findings move from Open to Excepted and send no alert. Revoking it, or letting it end, puts them back. Changing a mode, adding an exception and revoking one each take an owner or admin and are in the audit log.
Drift from a baseline
A resource’s first read is its baseline. When a later read differs in a setting that matters, the resource is marked drifted and listed under Drift with each changed field’s baseline and current value. Fields that change in normal use, such as sign-in times, status and versions, are left out, and a field the last read couldn’t see is never counted as a change. Accept the new settings as the baseline for one resource or for every drifted resource of a connector; whoever accepts it is recorded. The alert for new failures also says how many resources drifted in that sync.
Limits
| What | Limit |
|---|---|
| Connectors | 25 a workspace, one per account |
| Schedule | Daily unless set hourly or off |
| A sync | 15 minutes, 50,000 resources and 10,000 provider calls |
| Syncs by hand | one connector once every 5 minutes; 20 a workspace an hour |
| Settings kept per resource | 16 KB |
| Exceptions | 500 in force a workspace, one per check and resource, at most 365 days each |
| Evidence per finding | 6 fields |
| Drift per resource | 50 changed fields listed |
| Baselines accepted | 60 times a workspace an hour |
Connecting, changing and disconnecting take an owner or admin, and every change and every sync started by hand is in the audit log. The API is under /api/v1/shield/posture in the API reference.