Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,12 +317,15 @@ Two of these deserve a deliberate decision rather than a default:

**Artifact storage has two widths, and you pick between them by naming a bucket or not.** Leave
`artifact_storage_bucket` (Azure: `artifact_storage_account_name`) empty and Ringleader creates
and converges its own — confined on GCP and AWS to names beginning `ringleader-`, so the grant
reaches nothing you already have. Name one you created and the grant narrows to object access on
it, with no ability to create, reshape or delete a bucket at all: its location, its lifecycle
rules and its encryption key stay yours. That second width is the one to take if you have a
data-residency or key-custody position to defend — set the bucket's default encryption to a key
in your own KMS and every payload lands under it, with nothing on our side to configure.
and converges its own. On GCP and AWS those are confined to names beginning `ringleader-`, so the
grant reaches no bucket you already have (on AWS it can still list your buckets' names, regions
and creation dates). Name one you created and the grant narrows to object access on it, with no
ability to create, reshape or delete a bucket at all: its location, its lifecycle rules and its
encryption key stay yours. That second width is the one to take if you have a data-residency or
key-custody position to defend: set the bucket's default encryption to a key in your own KMS and
every payload lands under it, with nothing on our side to configure. On AWS that holds for a key in
the same account, and [the AWS README](aws/README.md#a-bucket-encrypted-with-a-kms-key-of-yours)
says what a stricter key policy needs.

With the switch off, payloads go to a bucket Ringleader owns, which is where they go today.
Nothing degrades and nothing fails.
Expand Down
55 changes: 41 additions & 14 deletions aws/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,17 @@ The permissions policy's **base** is exactly these four statements — no wildca
AWS-owned public AMI parameters.

Three features that are **on by default** add statements beside those four, so a role applied on
the defaults carries thirteen, not four, and both supported paths carry the same thirteen. All are
the defaults carries fifteen, not four, and both supported paths carry the same fifteen. All are
one variable away from off, and each is enumerated where it is described rather than here:
*[egress control](#optional-egress-control)* adds the security-group, subnet and route-table
writes, two security-group reads, the Elastic-IP actions and
`ec2:ModifyNetworkInterfaceAttribute`, bounded to your VPC and region; *workstation identities*
adds one `iam:PassRole`, bounded to roles under `workstation_identity_path` and to
`ec2.amazonaws.com`; *[artifact storage](#optional-artifact-storage-in-a-bucket-of-yours)* adds
the S3 bucket and object actions, bounded by ARN to buckets named `ringleader-*` or to the one
bucket you name. The `actions_granted` output is read from the same lists the policy is built
from, so it is the authority on what your role actually holds.
bucket you name, plus the bucket listing on the managed width and the use of this account's KMS
keys through S3 for those buckets. The `actions_granted` output is read from the same lists the
policy is built from, so it is the authority on what your role actually holds.

No `iam:*` unless you opt into per-workstation instance profiles
(`enable_workstation_identities` / `EnableWorkstationIdentities`, on by default and scoped to
Expand Down Expand Up @@ -493,15 +494,11 @@ plus workflow file outputs and files a workstation publishes. By default those b
bucket Ringleader owns. `enable_artifact_storage` (`ARTIFACT_STORAGE` on the CloudFormation
path), **on by default**, lets them go to an S3 bucket in this account instead.

**The S3 backend ships after the GCS one**, so on this cloud the grant deliberately arrives
before the feature. That is the point: it costs an unused account nothing, and it is what stops
S3 support from becoming a second apply for every customer later.

### The two widths, and how you pick

**Managed**: leave `artifact_storage_bucket` unset. Ringleader creates and converges its own
buckets, bounded by **ARN pattern** to `arn:aws:s3:::ringleader-*` and the objects in them, so the
grant reaches no bucket you already have.
buckets, bounded by **ARN pattern** to `arn:aws:s3:::ringleader-*` and the objects in them, so its
bucket and object actions reach no bucket you already have. It can list them, as described below.

The bound is a name prefix rather than a region, on purpose twice over: S3 is global, so a
region condition would break a bucket you deliberately placed elsewhere for residency, and a
Expand All @@ -519,25 +516,55 @@ is what makes key custody a decision Ringleader never touches.
in **both** widths. They let Ringleader report the bucket's encryption, its region and its
versioning state on the `Storage` object. Without them that status can only say `unknown`.

The grant is two statements in the named width and three in the managed one. S3 checks bucket
The grant is three statements in the named width and five in the managed one. S3 checks bucket
actions such as `s3:ListBucket` against the bucket's ARN and object actions against
`<bucket>/*`, so the two sets name different resources and sit in separate statements.
`s3:ListBucket` on an object ARN deploys cleanly and is refused on every call. The third
statement, managed width only, holds `CreateBucket`, `DeleteBucket`, the reads and writes for a
bucket's public access block, lifecycle and tags, `PutBucketVersioning` and `ListBucketVersions`.
`s3:ListBucket` on an object ARN deploys cleanly and is refused on every call. The third statement
in both widths is the KMS one described below. The other two are managed width only. One holds
`CreateBucket`, `DeleteBucket`, the reads and writes for a bucket's public access block, lifecycle
and tags, `PutBucketVersioning` and `ListBucketVersions`. The other holds `s3:ListAllMyBuckets`.

These managed-width actions let Ringleader configure a bucket it creates. With versioning on, S3
The provisioning actions let Ringleader configure a bucket it creates. With versioning on, S3
keeps the old versions of an object, and a lifecycle rule deletes them after a set time.
`PutBucketTagging` lets Ringleader tag a bucket with the `Storage` object it was created for, and
`GetBucketTagging` reads that tag back. That lets Ringleader tell a bucket it created from one it
did not. `ListBucketVersions` lets it check that a bucket holds no object versions or delete markers
before deleting it.

`s3:ListAllMyBuckets` lets Ringleader read the creation date of a bucket it created, so it can tell
that bucket from a new one with the same name that someone else creates after the first is deleted.
S3 reports a bucket's creation date in this listing and cannot limit the action to one bucket name.
So the action sits in a statement of its own on `*`, and it lists every bucket in the account. For
each bucket it returns the name, region and creation date, and never anything inside the bucket. It
is granted on the managed width alone, where you already let Ringleader create buckets.

The multipart actions (`AbortMultipartUpload`, `ListMultipartUploadParts`,
`ListBucketMultipartUploads`) are not extras. Any upload over the SDK's threshold becomes a
multipart upload, so a grant without them fails on a large transcript and on nothing else — the
worst shape of bug to find in production.

### A bucket encrypted with a KMS key of yours

S3 calls KMS with the caller's own permissions whenever it reads or writes an object encrypted with
a KMS key. So in **both** widths the role holds `kms:Decrypt` and `kms:GenerateDataKey` on KMS keys
in this account, under two conditions that must both hold:

- `kms:ViaService` matches `s3.*.amazonaws.com`, so the call comes through S3.
- The `aws:s3:arn` value in the encryption context S3 sends (condition key
`kms:EncryptionContext:aws:s3:arn`) names an artifact bucket the grant covers, or an object in one.

Through this grant, the role can use a key only by asking S3, and only for those buckets.

If your key has AWS's default key policy, which lets IAM policies in this account grant access to
it, your bucket needs nothing more. Switching the bucket to another key in this account does not
need a re-apply either.

If your key's policy does not let IAM grant access, add a statement to it that allows this role
`kms:Decrypt` and `kms:GenerateDataKey` under the same two conditions. A key policy statement needs
no IAM policy behind it, so without the conditions the role could use that key directly and for any
bucket. A key in another account is not covered: using one takes that key's policy and a policy of
your own on this role.

### What is deliberately withheld

`s3:PutBucketPolicy` and every ACL write. Nothing in this onboarding may **grant** authority, and
Expand Down
13 changes: 9 additions & 4 deletions aws/cloudformation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,15 @@ Two more grant capabilities on the role rather than on the network. `EnableWorks
account rather than in its own; `ArtifactStorageBucket` (empty) takes the managed width, where
Ringleader creates and converges its own buckets bounded by ARN to names beginning
`ringleader-`, and naming a bucket you made narrows the grant to that one ARN with no
`CreateBucket`, `DeleteBucket` or lifecycle write. On the managed width
`ArtifactStorageBucketPrefix` (empty) takes a short label of yours that those buckets must carry in
their name, bounding the ARNs to `ringleader-<label>*` instead of every `ringleader-*` bucket in
the account; set it when one account serves several Ringleader organizations. `deploy.sh` exposes
`CreateBucket`, `DeleteBucket` or lifecycle write. On the managed width the role may also list the
account's buckets (`s3:ListAllMyBuckets`), which shows each one's name, region and creation date and
nothing inside it. In both widths it may use a KMS key in this account through S3, for the artifact
buckets alone, so a bucket encrypted with a key of yours needs nothing more unless that key's policy
does not let IAM grant access. `aws/README.md` says what to add to such a key's policy. On the
managed width `ArtifactStorageBucketPrefix` (empty) takes a short label of yours that those
buckets must carry in their name, bounding the ARNs to `ringleader-<label>*` instead of every
`ringleader-*` bucket in the account; set it when one account serves several Ringleader
organizations. `deploy.sh` exposes
all five as `WORKSTATION_IDENTITIES`, `WORKSTATION_IDENTITY_PATH`, `ARTIFACT_STORAGE`,
`ARTIFACT_STORAGE_BUCKET` and `ARTIFACT_STORAGE_BUCKET_PREFIX`. The last is passed only when set,
like the flow-log parameters, so a run that does not mention it keeps the stack's rather than
Expand Down
35 changes: 32 additions & 3 deletions aws/cloudformation/ringleader-onboarding.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,12 @@ Parameters:
Default: ""
Description: >-
The one S3 bucket Ringleader may write artifact payloads to, when you would rather own it
than let Ringleader create one. Empty -- the default -- takes the managed width instead.
than let Ringleader create one. Empty (the default) takes the managed width instead.
Set it to a bucket that already exists in this account: the grant is then bound to that
bucket's ARN alone and carries no CreateBucket, DeleteBucket or lifecycle write, so its
region, its lifecycle rules and its default encryption stay yours. Hand the same name back
to Ringleader as the Storage object's spec.bucket.
region, its lifecycle rules and its default encryption stay yours. A KMS key in this account
works through S3 unless its key policy bars IAM (see aws/README.md). Hand the same name
back to Ringleader as the Storage object's spec.bucket.

EnableEgressControl:
Type: String
Expand Down Expand Up @@ -707,6 +708,34 @@ Resources:
- !Sub "arn:${AWS::Partition}:s3:::${ArtifactStorageBucket}/*"
- !Ref AWS::NoValue

- !If
- WantArtifactStorageManaged
- Sid: ArtifactStorageBucketListing
Effect: Allow
Action:
- "s3:ListAllMyBuckets"
Resource: "*"
- !Ref AWS::NoValue

- !If
- WantArtifactStorage
- Sid: ArtifactStorageKeys
Effect: Allow
Action:
- "kms:Decrypt"
- "kms:GenerateDataKey"
Resource: !Sub "arn:${AWS::Partition}:kms:*:${AWS::AccountId}:key/*"
Condition:
StringLike:
"kms:ViaService": "s3.*.amazonaws.com"
"kms:EncryptionContext:aws:s3:arn": !If
- ArtifactStorageIsManaged
- !Sub "arn:${AWS::Partition}:s3:::ringleader-${ArtifactStorageBucketPrefix}*"
-
- !Sub "arn:${AWS::Partition}:s3:::${ArtifactStorageBucket}"
- !Sub "arn:${AWS::Partition}:s3:::${ArtifactStorageBucket}/*"
- !Ref AWS::NoValue

Vpc:
Type: AWS::EC2::VPC
Condition: WantNetwork
Expand Down
4 changes: 2 additions & 2 deletions aws/terraform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ output "handoff" { value = module.ringleader_onboarding.handoff }
| `create_nat_gateway` | **`true`** | a NAT gateway **and a private route table**, for workstations with no public IP. **The one default that costs money** — hourly plus $0.045/GB. The proxy replaces it later |
| `enable_egress_control` | **`true`** | let Ringleader manage the security groups an egress policy compiles to, **and** the subnets and route tables that steer traffic at the proxy. Grants seven security-group actions, nine route-table and subnet writes, `ec2:ModifyNetworkInterfaceAttribute`, the five Elastic IP actions that reserve the gateway's address, and two reads. Restricts nothing until you declare a policy. Also lets Ringleader create the security group that admits SSH to its workstations |
| `egress_vpc_ids` | `[]` | confine those permissions to these VPCs. Empty uses the VPC this module created; empty **and** no created network means scoping by `allowed_regions` only, or none if that is empty too. Check the `egress_scope` output |
| `enable_artifact_storage` | **`true`** | let Ringleader hold artifact payloads — sealed agent-session transcripts, workflow file outputs, files a box publishes — in an S3 bucket in **this account** rather than in Ringleader's own. Bounded by **ARN pattern** to buckets named `ringleader-*`, not by region: S3 is global, and a region bound would break a bucket you placed elsewhere for residency. Never `PutBucketPolicy` and never an ACL write. Writes nothing until a Ringleader namespace declares a `Storage` naming a bucket, and the S3 backend itself ships after the GCS one — the grant arrives early so that does not cost a second apply |
| `artifact_storage_bucket` | `""` (managed) | name a bucket **you** created and the grant narrows to that one ARN, with no `CreateBucket`, `DeleteBucket` or lifecycle write — its region, lifecycle rules and default encryption stay yours. Empty takes the managed width, where Ringleader creates and converges its own `ringleader-*` buckets |
| `enable_artifact_storage` | **`true`** | let Ringleader hold artifact payloads (sealed agent-session transcripts, workflow file outputs, files a box publishes) in an S3 bucket in **this account** rather than in Ringleader's own. Bucket and object actions are bounded by **ARN pattern** to buckets named `ringleader-*`, not by region: S3 is global, and a region bound would break a bucket you placed elsewhere for residency. Two grants reach further. On the managed width, `s3:ListAllMyBuckets` reads the name, region and creation date of every bucket in the account. In both widths, `kms:Decrypt` and `kms:GenerateDataKey` use this account's KMS keys, but only through S3 and only for the artifact buckets. Never `PutBucketPolicy` and never an ACL write. Writes nothing until a Ringleader namespace declares a `Storage` naming a bucket |
| `artifact_storage_bucket` | `""` (managed) | name a bucket **you** created and the grant narrows to that one ARN, with no `CreateBucket`, `DeleteBucket`, lifecycle write or bucket listing. Its region, lifecycle rules and default encryption stay yours, and the role may use the bucket's KMS key through S3 if the key is in this account. See `aws/README.md` if its key policy does not let IAM grant access. Empty takes the managed width, where Ringleader creates and converges its own `ringleader-*` buckets |
| `artifact_storage_bucket_prefix` | `""` | on the managed width, a short label of yours that those buckets must carry in their name, so the grant reaches only `ringleader-<label>*` instead of every `ringleader-*` bucket in the account. Empty keeps the wider bound. Set it when one account serves several Ringleader organizations |
| `create_gateway_subnet` | **`true`** | reserve an empty **public** subnet, plus its route table, for the DNS / HTTPS proxy VM. You hand its id back as `spec.subnet` on the `Edge`, and Ringleader builds no gateway until you do. Public and in the workstations AZ are both cost decisions — see `aws/README.md` |
| `create_governed_subnet` | **`true`** | reserve the subnet the workstations that proxy **governs** go in — the 15th `/20` of the VPC, `10.60.224.0/20` at index 0. Empty, no route table (Ringleader claims it and refuses one already associated) and no public IPs — so a box in it has no egress until a proxy steers it. See `aws/README.md` |
Expand Down
Loading
Loading