Security

plainfra access and revocation

What the plainfra read-only role can see in your AWS account, what it cannot, and how to remove it.

This is written for the person who approves the access rather than the person who installs it. Every claim below can be checked with a read-only command against your own account, and section 5 gives you those commands. Placeholders: ACCOUNT_ID (your 12-digit AWS account ID), EXTERNAL_ID (the value plainfra issues you), REGION (the region you deployed the stack in), OU_ID (the organisational unit the account sits in, or your organisation root ID if you have no organisational units, format r-xxxx).

The install creates exactly one resource: an IAM role named plainfra-ReadOnly. No buckets, no functions, no users, no other roles. Source of truth is the CloudFormation template itself, published at customer-role.yaml. Read it before you deploy it; we encourage that.

1. The off switch

Deleting the CloudFormation stack deletes the role. Run this in the account you want to remove, one command, no arguments to prepare:

aws cloudformation delete-stack --stack-name plainfra-ReadOnly --region REGION

For accounts installed through StackSets, remove the stack instance instead:

aws cloudformation delete-stack-instances --stack-set-name plainfra-readonly --deployment-targets OrganizationalUnitIds=OU_ID,Accounts=ACCOUNT_ID,AccountFilterType=INTERSECTION --regions REGION --no-retain-stacks --region REGION

The organisational unit and the account filter are both required here, even when you are removing a single account. A service-managed StackSet only accepts an organisational unit as its target, so passing Accounts= on its own is rejected with ValidationError: StackSets with SERVICE_MANAGED permission model can only have OrganizationalUnit as target. If your organisation is flat and has no organisational units, use the organisation root ID (format r-xxxx) as OU_ID, which is a valid target.

Either command returns immediately and the role is gone in about a minute. That is the complete removal: no residue, nothing to clean up afterwards, no coordination with plainfra required. You do not need to tell us, and you do not need our agreement.

Two things worth being straight about.

Disconnecting the account in the plainfra console does not revoke AWS access. It stops plainfra querying the account and frees the account ID for future reconnection, but the role and its trust policy stay exactly as they are, still assumable. plainfra simply stops calling sts:AssumeRole because it no longer has a reason to. If you want the access gone rather than unused, delete the stack. That is the only step that removes anything.

Cutting access without deleting the role takes two commands, not one. Blocking the trust policy stops new sessions being issued. It does not touch sessions that already exist, because AWS does not retroactively invalidate session tokens when a trust policy changes. Two bounds decide how long that gap lasts: role sessions are capped at one hour (MaxSessionDuration: 3600), and plainfra caches assumed-role credentials in a warm process for up to 45 minutes.

We have measured this rather than reasoned about it. With a deny-all trust policy applied to the role and a fresh sts:AssumeRole correctly refused within about eight seconds, a query against that same account still returned live data from cached credentials. If you use the trust-policy edit on its own as an incident stop, you have a window, and it can be tens of minutes long.

To stop access now, run both steps in this order.

First, block the trust policy:

aws iam update-assume-role-policy --role-name plainfra-ReadOnly --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}'

Then invalidate the sessions already issued, which is the step that closes the window. AWS provides this explicitly: an AWSRevokeOlderSessions inline policy denying everything issued before a given aws:TokenIssueTime. The IAM console exposes it as "Revoke active sessions" on the role, and the same policy can be attached with iam:PutRolePolicy (AWS documentation).

Neither step alone is sufficient. The first without the second leaves existing sessions working. The second without the first stops current sessions but lets a new one be issued straight away.

To restore access afterwards, put the original trust policy back:

aws iam update-assume-role-policy --role-name plainfra-ReadOnly --policy-document '{"Version":"2012-10-17","Statement":[{"Sid":"AllowPlainfraAssumeWithExternalId","Effect":"Allow","Principal":{"AWS":["arn:aws:iam::921514166437:root","arn:aws:iam::121754142582:root"]},"Action":"sts:AssumeRole","Condition":{"StringEquals":{"sts:ExternalId":"EXTERNAL_ID"}}}]}'

If you revoked sessions as well, remove the AWSRevokeOlderSessions inline policy from the role, or it keeps denying.

All of this leaves the CloudFormation stack in a drifted state, which is fine for an incident and wrong as a permanent removal. If you are finished with plainfra rather than pausing it, delete the stack instead. Deleting the role removes the thing sessions are issued against, so there is nothing left to revoke.

2. What the role can see

Every permission is listed explicitly in the template. No AWS managed policies are attached, and there is no *:* grant anywhere. The allow statements are Describe*, List* and Get* style calls across four groups:

Compute, containers, serverless. EC2, Elastic Load Balancing, Auto Scaling, Application Auto Scaling, ECS, EKS, ECR (repository and policy metadata), Lambda configuration, Step Functions, App Runner.

Storage and databases. S3 bucket metadata and listings, EFS, AWS Backup, RDS, DynamoDB table metadata, ElastiCache, Redshift.

Identity, security, compliance. IAM, CloudTrail (including LookupEvents), AWS Config, GuardDuty, Security Hub, IAM Access Analyzer, Secrets Manager metadata, Systems Manager metadata and inventory, KMS metadata.

Networking, observability, cost, governance. CloudFront, Route 53 (including domains and resolver), ACM, WAFv2, API Gateway (GET only), CloudWatch, CloudWatch Logs metadata, X-Ray, SNS, SQS, EventBridge and Scheduler, Kinesis, Firehose, CloudFormation, Cost Explorer, Cost and Usage Reports, Budgets, AWS Organizations, Account, Support, Trusted Advisor, AWS Health, resource tags.

No write, create, update, start, stop, or delete action is granted anywhere in the template. There is nothing in the role that can change your infrastructure.

Billing console access (aws-portal:ViewBilling, billing:Get*, billing:ListBillingViews) is off by default and only present if whoever deployed the stack set AllowBillingConsoleRead=true. Cost Explorer (ce:Get*) is in the base role either way, because it is where cost answers come from. Section 5 shows you how to check which setting was used.

3. What the role cannot see

A separate deny policy sits on top of the allow statements. In IAM, an explicit deny always wins, so these hold regardless of what a wildcard in section 2 technically matches. These are the exact statements, and they are the part worth checking yourself.

Data contents. s3:GetObject, s3:GetObjectVersion, s3:GetObjectTorrent, s3:GetObjectVersionTorrent, s3:SelectObjectContent, dynamodb:GetItem, dynamodb:BatchGetItem, dynamodb:Query, dynamodb:Scan, dynamodb:GetRecords, dynamodb:PartiQLSelect, kinesis:GetRecords, kinesis:GetShardIterator, kinesis:SubscribeToShard, sqs:ReceiveMessage, ecr:GetDownloadUrlForLayer, ecr:BatchGetImage.

plainfra can see that a bucket exists, how big it is and how it is configured. It cannot read a file out of it. It can see a DynamoDB table's schema and item count. It cannot read a row.

Secrets, parameters, logs, decryption. secretsmanager:GetSecretValue, ssm:GetParameter, ssm:GetParameters, ssm:GetParametersByPath, ssm:GetParameterHistory, logs:GetLogEvents, logs:FilterLogEvents, logs:StartQuery, logs:StopQuery, logs:GetQueryResults, logs:StartLiveTail, kms:Decrypt, kms:GenerateDataKey, kms:GenerateDataKeyPair, kms:GenerateDataKeyWithoutPlaintext.

Log group names, retention settings and stored volume are visible. Log lines are not. Secret names are visible. Secret values are not. Nothing in the role can decrypt anything.

Code, execution, console access. lambda:InvokeFunction, lambda:InvokeFunctionUrl, lambda:GetFunction, cloudformation:GetTemplate, states:GetExecutionHistory, ec2:GetConsoleOutput, ec2:GetConsoleScreenshot, ec2:GetPasswordData, rds-data:ExecuteStatement, rds-data:BatchExecuteStatement, glacier:GetJobOutput, glacier:InitiateJob.

plainfra cannot run anything. It cannot invoke a function, download a deployment package, fetch a CloudFormation template body, read EC2 console output or screenshots, retrieve a Windows password, or execute SQL through the RDS Data API.

API key values. apigateway:GET is allowed across API Gateway generally, and denied specifically against arn:aws:apigateway:*::/apikeys and arn:aws:apigateway:*::/apikeys/*.

4. The edges you should know about

Section 2 grants metadata. Some of that metadata is sensitive in its own right, and you will find this yourself in about twenty minutes of reading the template, so here it is up front.

  • Your IAM graph is fully readable. iam:Get* and iam:List* cover users, roles, groups, inline and attached policy documents, MFA device listings, and iam:GenerateCredentialReport. plainfra can enumerate your entire identity posture. That is deliberate, because it is what identity findings are made of, but it means the role can read who has access to what.
  • CloudTrail event history is readable. cloudtrail:LookupEvents returns management events: who did what, when, from which IP address. That is activity data about your staff, not just configuration.
  • Secret and parameter names are readable even though values are not. secretsmanager:ListSecrets, secretsmanager:DescribeSecret and the SSM Describe*/List* calls expose names, descriptions, rotation configuration and tags. Names sometimes describe what the secret is for.
  • S3 configuration is readable. Bucket names, policies, ACLs, encryption settings, lifecycle rules and public-access settings. Not object contents.
  • All resource tags are readable. tag:Get* covers every tag on every resource. Tags commonly carry owner names, email addresses, cost centres and project codenames.
  • Security findings are readable. GuardDuty, Security Hub, AWS Config and Access Analyzer results, including open findings. This is the current state of your security posture, including its gaps.

If any of those is unacceptable for a given account, the EC2-only variant (customer-role-lite.yaml, published alongside the full template) grants exactly one permission, ec2:Describe*, and nothing else. It uses the same role name and trust policy, so moving from lite to full later is an in-place stack update rather than a fresh install.

5. Verifying all of this yourself

Every command here is read-only. Run them in the target account against your own role. None of them go through plainfra.

Confirm no AWS managed policies are attached, which is the fastest way to bound what the role can do:

aws iam list-attached-role-policies --role-name plainfra-ReadOnly

That should return an empty list. Then list the inline policies actually present:

aws iam list-role-policies --role-name plainfra-ReadOnly

Expect PlainfraInfrastructureDiscovery and PlainfraDataAccessDeny, plus PlainfraBillingConsoleRead only if billing console access was enabled at deploy time. Read the deny policy in full:

aws iam get-role-policy --role-name plainfra-ReadOnly --policy-name PlainfraDataAccessDeny

Check who can assume the role and under what condition:

aws iam get-role --role-name plainfra-ReadOnly --query 'Role.AssumeRolePolicyDocument'

Test the denies directly rather than reading them. IAM policy simulation evaluates the real policy set and tells you the effective decision per action:

aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::ACCOUNT_ID:role/plainfra-ReadOnly --action-names s3:GetObject secretsmanager:GetSecretValue logs:FilterLogEvents lambda:InvokeFunction kms:Decrypt cloudformation:GetTemplate --output table

Every one of those should come back implicitDeny or explicitDeny. Add any action you want to satisfy yourself about.

Confirm which parameters the stack was actually deployed with, including whether billing console read was turned on. The command depends on how the role was installed, because a StackSet does not name its stacks the way a direct install does.

If the role was installed directly into this account, with CloudShell or the console quick-create, the stack is named plainfra-ReadOnly:

aws cloudformation describe-stacks --stack-name plainfra-ReadOnly --region REGION --query 'Stacks[0].Parameters'

If it was installed across your organisation with StackSets, that command fails with ValidationError: Stack with id plainfra-ReadOnly does not exist even though the role is present and healthy, because the stack is named StackSet-plainfra-readonly-<uuid>. Ask the StackSet for this account's stack instead, from the management or delegated administrator account:

aws cloudformation describe-stack-instance --stack-set-name plainfra-readonly --stack-instance-account ACCOUNT_ID --stack-instance-region REGION --region REGION

That returns the instance's StackId along with the parameter values in effect for this account. If you want the full stack view, run describe-stacks --stack-name against the returned StackId.

A failure here is a naming mismatch, not a security finding. Confirm the role itself with get-role and list-role-policies above before concluding anything is wrong.

After access has been in place for a while, audit what we actually called. Every plainfra session assumes the role with a session name prefixed plainfra-, and the specific names describe the purpose: plainfra-session for chat and reports, plainfra-collector for scheduled scans, plainfra-verify and plainfra-verify-account for connection checks, plainfra-upgrade-probe for a lite-to-full upgrade check. Those names appear in CloudTrail against every call:

aws cloudtrail lookup-events --lookup-attributes AttributeKey=ResourceName,AttributeValue=plainfra-ReadOnly --max-results 50 --region REGION

This is the check that matters most, because it is the only one that tells you what happened rather than what was permitted.

6. What you are actually trusting

Two things, and it is worth naming both.

The trust policy points at plainfra's AWS accounts, not at a named role inside them. The principals are arn:aws:iam::921514166437:root and arn:aws:iam::121754142582:root, both owned by Triont and both listed so plainfra can move between its own accounts without asking you to redeploy. Naming an account delegates to plainfra's own IAM: any principal in that account that is itself permitted to call sts:AssumeRole against your role can do so. This is the standard cross-account pattern, and it means our internal IAM discipline is part of your trust boundary.

The External ID is the second factor, and it is one value per customer. The trust policy requires a matching sts:ExternalId, so knowing the role ARN alone is not enough to assume it. plainfra issues one External ID per customer and reuses it across every AWS account you connect, which is what allows a single StackSet parameter set to deploy everywhere. The consequence is that one value protects all your connected accounts. Treat it as a credential.

Both of these are bounded by section 3: even a successful assumption cannot read your data, your secrets, your logs, or your code, and cannot change anything.

Questions on any of this: support@plainfra.com, or ask Tim directly.