Connect your own infrastructure
Part of the PerspectiveGraph manual. Where to run the engine for the AWS accounts and Kubernetes clusters you have, and how each of them reaches it.
The engine learns an estate in two ways. It reads some of it itself, read-only and on a
schedule: the AWS connector reads accounts, and the Kubernetes connector reads the cluster the
engine is installed in. Everything else is sent to it: other clusters, scanner reports and
the merge gate’s pull requests, posted by perspectivegraph ingest or the GitHub Action. Where
the engine runs decides which of the two each source takes, so this page goes by place:
| On a laptop | On one machine | On Kubernetes | |
|---|---|---|---|
| For | a look at your own account, this afternoon | a team’s instance, without a cluster | production beside your workloads |
| Install | make up-aws |
make prod-init DOMAIN=<name> |
the Helm chart |
| AWS accounts | read with your profile | read with the instance’s role, or a profile | read with the pod’s role (EKS Pod Identity or IRSA) |
| The cluster it runs in | - | - | read by the Kubernetes connector |
| Other clusters | a dump sent from the laptop | a dump sent by cron or CI | a dump sent by cron or CI |
| Database | bundled, throwaway | bundled, dumped daily, restore tested | CloudNativePG: replicas, failover, point-in-time recovery |
Whatever the place, the account is read through one role: deploy/aws
has it as CloudFormation and as Terraform - the AWS-managed SecurityAudit policy, two EKS
Pod Identity reads, nothing that writes - and the read-only role
says who may assume it in each case. Deploy it in every account you want read.
The engine does not scan anything. Image and code findings come from the scanners you already run (Trivy, Semgrep, Falco, Cloud Custodian), sent as they are: the onboarding runbook has a recipe for each, and the order that builds a correct graph.
On a laptop
Section titled “On a laptop”For your own account, today, with nothing deployed: Docker, a clone of the repository at the release, and an AWS profile.
git clone --branch v1.38.1 https://github.com/luiacuaniello/perspectivegraph.git # x-release-please-versioncd perspectivegraphAWS_PROFILE=<profile> AWS_REGION=eu-west-1 make up-awsmake up-aws runs the published images with your ~/.aws mounted read-only, checks first
that the backend can read it, and waits for the first pull of the account. Better still, read
as the role rather than as yourself: deploy it with TrustedPrincipalArn set to your user or
role, and add AWS_ROLE_ARN=<the role's ARN> to the command. The details, SSO profiles and
Linux included, are in From your own machine.
The dashboard is on http://localhost:3000.
A cluster, from the same laptop: dump it with kubectl and send the dump. The CLI is a
single binary from the releases page,
and it posts to the local stack by default:
kubectl get ingress,service,pod,serviceaccount,role,clusterrole,rolebinding,clusterrolebinding,node -A -o json | perspectivegraph ingest -cluster prod-eu -snapshot cluster:prod-eu k8s -On EKS, name the cluster by its EKS name: that is how its pods meet the Pod Identity
associations and access entries the AWS connector reads, so a route can run from a pod into
the account. -snapshot says the dump is the whole cluster, so what it no longer holds next
time leaves the graph.
This stack is the demo profile: no credentials, and nothing published beyond 127.0.0.1. It
holds your account’s attack paths, so keep it there, and erase it when you are done:
docker compose -f docker-compose.yml -f docker-compose.demo.yml --profile app down -v.
On one machine
Section titled “On one machine”make prod-init DOMAIN=<name> on a VM with Docker sets up the rest: TLS that renews itself,
every credential generated into files, the backend refusing to start without them, NATS with
a password, a daily backup and a restore tested in CI on every change. Production on one
machine is the whole procedure. Then connect the estate:
AWS, from an EC2 instance. The backend reads the account with the instance’s own role: add
the read-only policy to that role, or deploy the read-only role trusting it and set
AWS_ROLE_ARN. Add to .env, then docker compose up -d:
CONNECTORS_ENABLED=awsAWS_CONNECTOR_MODE=sdkAWS_REGION=eu-west-1# AWS_ROLE_ARN=arn:aws:iam::111111111111:role/PerspectiveGraphReadOnlyThe backend is a container, one network hop further from the instance metadata service than the instance itself, and IMDSv2’s default hop limit of 1 drops its answers. Raise it to 2, keeping tokens required:
aws ec2 modify-instance-metadata-options --instance-id <id> \ --http-tokens required --http-put-response-hop-limit 2AWS, from a machine elsewhere - another cloud, a server of your own. Append
:docker-compose.aws.yml to COMPOSE_FILE in .env, and the backend reads with a profile from
the ~/.aws of the user who runs docker compose, mounted read-only.
Clusters are sent, from wherever kubectl reaches them - a CI job, or cron on a bastion.
The address is the machine’s, /ingest behind Caddy, and the secret is the one prod-init
generated (secrets/ingest_hmac_secret):
# crontab: every 30 minutes*/30 * * * * kubectl get ingress,service,pod,serviceaccount,role,clusterrole,rolebinding,clusterrolebinding,node -A -o json | INGEST_HMAC_SECRET_FILE=/etc/perspectivegraph/ingest_hmac_secret perspectivegraph ingest -url https://perspectivegraph.example.com -cluster prod-eu -snapshot cluster:prod-eu k8s -Pull requests get their verdict from the merge gate, pointed at the same address.
On Kubernetes
Section titled “On Kubernetes”On EKS, with its AWS account
Section titled “On EKS, with its AWS account”The engine runs beside the workloads it reads: the Kubernetes connector reads the cluster, the AWS connector reads the account with a role bound to the backend alone, and the database is run by CloudNativePG with replicas and continuous backups. Four steps, the first two once per cluster.
1. The CloudNativePG operator - and, for backups, its Barman Cloud plugin and a bucket: A production database has both.
kubectl apply --server-side -f \ https://github.com/cloudnative-pg/cloudnative-pg/releases/download/v1.30.1/cnpg-1.30.1.yaml2. The read-only role, for the backend’s service account. EKS Pod Identity needs its
agent on the cluster. The association names the service account the chart creates,
<release>-perspectivegraph-backend, and can be made before the install:
aws eks create-addon --cluster-name prod-eu --addon-name eks-pod-identity-agentaws cloudformation deploy --stack-name perspectivegraph-readonly \ --template-file deploy/aws/readonly-role.cfn.yaml --capabilities CAPABILITY_NAMED_IAM \ --parameter-overrides TrustEksPodIdentity=trueaws eks create-pod-identity-association --cluster-name prod-eu \ --namespace perspectivegraph --service-account perspective-perspectivegraph-backend \ --role-arn <the stack's RoleArn output>IRSA works too: let the role trust the cluster’s OIDC provider instead, and put its ARN in
serviceAccount.annotations.
3. The chart, from the production profile it ships with:
helm pull oci://ghcr.io/luiacuaniello/charts/perspectivegraph --untar \ --version 1.38.1 # x-release-please-versionhelm install perspective oci://ghcr.io/luiacuaniello/charts/perspectivegraph \ --namespace perspectivegraph --create-namespace \ -f perspectivegraph/values-production.yaml \ --set secrets.existingSecret= --set secrets.generate=true \ --set postgres.cloudnativepg.enabled=true \ --set governanceBackend=postgres \ --set 'connectors.enabled={aws,kubernetes}' \ --set connectors.aws.mode=sdk --set connectors.aws.region=eu-west-1 \ --set connectors.kubernetes.clusterName=prod-eu \ --set-string connectors.kubernetes.awsAccount=111111111111 \ --set ingress.enabled=true --set ingress.host=perspectivegraph.example.com \ --set ingress.tls.enabled=true \ --set-string 'ingress.annotations.cert-manager\.io/cluster-issuer=letsencrypt-prod' \ --set backend.trustedProxyCidrs=10.0.0.0/16 \ --version 1.38.1 # x-release-please-versionWhat each line does:
values-production.yamlis the hardened profile:PG_ENV=production, network policies, a verified TLS connection to the database.secrets.generatefills every credential it needs - an admin token, the ingest secret, the at-rest encryption key - and keeps them across upgrades; the release notes print how to read the token and the ingest secret back. Back the Secret up: its encryption key is the only key to the encrypted stores. Under Argo CD or Flux, which render without the cluster, bring the Secret yourself instead (why).postgres.cloudnativepg.enabledhands the database to the operator, andgovernanceBackend=postgresputs triage, tickets, verdicts and the audit chain in it, so they have its replicas and backups too. Addpostgres.cloudnativepg.backup.objectStoreonce the bucket exists - without it there are no backups, and the release notes say so.connectors.kubernetes.clusterNameis the EKS cluster’s name, andawsAccountthe account its nodes run in. It is a string:--setwould read the account id as a number, which the chart’s schema refuses.connectors.awsreads the account with the role from step 2, with no keys anywhere.- The ingress publishes the dashboard, the API and
/ingest- the chart refuses to publish them without credentials.backend.trustedProxyCidrsis where the ingress controller connects from (on EKS, pod addresses come from the VPC), so rate limits and lockouts count each client rather than the controller.
4. Check it reads:
kubectl -n perspectivegraph port-forward svc/perspective-perspectivegraph-backend 8081:8081 &curl -s localhost:8081/connectorsEach connector reports its last run, whether it succeeded, the error if not, and how many events it produced. A Pod Identity that is not working shows here as a credentials error from the AWS connector, not as an empty board.
Any other Kubernetes
Section titled “Any other Kubernetes”GKE, AKS, a cluster of your own: the same chart. The Kubernetes connector reads any cluster, and CloudNativePG runs on any of them - leave out the AWS lines, and keep the operator for the database: Apache AGE is a managed offering on Azure alone (the database matrix). Of the clouds themselves, only AWS is read live today: the Azure connector reads exported files, and there is no GCP connector.
More clusters and more accounts
Section titled “More clusters and more accounts”Clusters. The connector reads the cluster the engine runs in. Every other cluster sends
its dump, from a job that can read it - the same kubectl get … | perspectivegraph ingest
as above, with -url https://perspectivegraph.example.com, its own -cluster name and the
ingest secret from the chart’s Secret.
Accounts. One role in each account, all trusting the backend’s own role; on that role,
AssumableRoleArns lists them (the role’s table). Then
name them all, comma-separated - escaped, because --set splits on commas:
--set 'connectors.aws.roleArn=arn:aws:iam::111111111111:role/PerspectiveGraphReadOnly\,arn:aws:iam::222222222222:role/PerspectiveGraphReadOnly'Each account’s assets are qualified with its id, so two accounts that reuse an identifier stay two, and one account whose role fails costs that account alone.
Is it connected?
Section titled “Is it connected?”Two questions, and an empty board answers neither.
Did each connector read? GET /connectors on the ingest port, as above - on one machine,
docker compose exec frontend wget -qO- http://backend:8081/connectors.
What has been fed, and how recently? ingestCoverage, with any API token:
curl -s https://perspectivegraph.example.com/graphql -H 'Content-Type: application/json' \ -H "Authorization: Bearer $API_TOKEN" \ -d '{"query":"{ ingestCoverage { source events nodes lastSeen stale } }"}'A source that is missing, or stale, is a feed to fix, not a finding. The first attack paths
appear after the next analyzer pass once a scanner report and the topology it sits in have both
arrived - the order that builds a correct graph
says which is which.