Με δύο λόγια: φτιάχνετε έναν IAM OIDC identity provider για το token.actions.githubusercontent.com, ένα IAM role του οποίου το trust policy δέχεται tokens μόνο από το δικό σας repository και branch (ή environment), δίνετε στο workflow permissions: id-token: write και καλείτε το aws-actions/configure-aws-credentials με role-to-assume. Κανένα secret δεν μένει στο GitHub και κάθε credential λήγει μόνο του.
Παρακάτω θα δείτε κάθε βήμα με Terraform, τη διαδρομή από το console, ένα least-privilege policy για ένα τυπικό container deploy, τα errors που σίγουρα θα συναντήσετε και πώς το απλώνετε σε πολλά AWS accounts.
Γιατί τα μόνιμα access keys είναι ρίσκο
Ένα access key IAM user ισχύει μέχρι να το κάνει κάποιος rotate ή να το σβήσει, και στην πράξη κανείς δεν το κάνει. Αντιγράφεται στα GitHub secrets, σε κάποιο laptop, σε μεταβλητή άλλου CI και καμιά φορά σε μήνυμα στο Slack. Όταν διαρρεύσει από κάπου, ο επιτιθέμενος έχει την ίδια πρόσβαση με το pipeline σας, συχνά AdministratorAccess, χωρίς ημερομηνία λήξης.
Το OIDC αλλάζει το μοντέλο. Σε κάθε run, το GitHub εκδίδει ένα υπογεγραμμένο JSON Web Token που λέει ακριβώς ποιος ζητάει πρόσβαση: repository, branch ή tag, environment, event. Το AWS STS επαληθεύει το token και επιστρέφει προσωρινά credentials για ένα role. Το default session κρατάει μία ώρα. Δεν υπάρχει τίποτα για rotate και τίποτα χρήσιμο να κλέψει κανείς από τα settings του repository.
Σας δίνει επίσης κάτι που τα keys δεν έδιναν ποτέ: να ορίσετε ότι «μόνο το main αυτού του repository κάνει deploy σε production» και να το επιβάλλει το ίδιο το AWS.
Βήμα 1: δημιουργήστε τον OIDC identity provider
Χρειάζεστε έναν IAM OIDC provider ανά AWS account, με URL https://token.actions.githubusercontent.com και audience sts.amazonaws.com. Τον φτιάχνετε μία φορά και τον χρησιμοποιούν όλα τα roles του account.
Με Terraform
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
}
Θα δείτε ακόμα tutorials με hard-coded thumbprint_list. Δεν χρειάζεται πια. Το AWS επαληθεύει το JWKS endpoint του GitHub με τη δική του λίστα από trusted root certificate authorities και γυρίζει σε thumbprints μόνο για providers που δεν χρησιμοποιούν κάποια από αυτές (δείτε την τεκμηρίωση του IAM για OIDC providers). Στον Terraform AWS provider το argument είναι προαιρετικό, και το README του configure-aws-credentials γράφει ρητά ότι όποιο thumbprint βάλετε αγνοείται.
Από το console
- Ανοίξτε το IAM, πηγαίνετε στα Identity providers και πατήστε Add provider.
- Επιλέξτε OpenID Connect.
- Provider URL:
https://token.actions.githubusercontent.com. Audience:sts.amazonaws.com. - Πατήστε Add provider.
Αν ο provider υπάρχει ήδη (κάποιος έκανε δοκιμές πριν από έναν χρόνο), κάντε τον import στο Terraform αντί να φτιάξετε δεύτερο. Το IAM επιτρέπει μόνο έναν provider ανά URL σε κάθε account.
Βήμα 2: ένα trust policy που περιορίζει πραγματικά
Το trust policy πρέπει να ελέγχει δύο claims: ότι το token.actions.githubusercontent.com:aud ισούται με sts.amazonaws.com, και ότι το token.actions.githubusercontent.com:sub ταιριάζει ακριβώς με το repository και το branch ή environment που θέλετε. Η ασφάλεια βρίσκεται στο sub.
Ένα role που μπορεί να το κάνει assume μόνο το main του acme/api:
data "aws_iam_policy_document" "github_trust" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.github.arn]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:acme/api:ref:refs/heads/main"]
}
}
}
resource "aws_iam_role" "github_deploy" {
name = "github-deploy-api"
assume_role_policy = data.aws_iam_policy_document.github_trust.json
}
Ποιο sub θα πάρετε;
Το GitHub φτιάχνει το sub διαφορετικά ανάλογα με το job, και από εδώ προέρχονται τα περισσότερα «not authorized» errors. Σύμφωνα με το OIDC reference του GitHub:
| Context του job | sub claim |
|---|---|
Το job έχει environment: production | repo:acme/api:environment:production |
| Push σε branch, χωρίς environment | repo:acme/api:ref:refs/heads/main |
| Push tag, χωρίς environment | repo:acme/api:ref:refs/tags/v1.4.0 |
| Pull request event, χωρίς environment | repo:acme/api:pull_request |
Το environment υπερισχύει. Μόλις ένα job δηλώσει environment:, το branch εξαφανίζεται από το sub, οπότε ένα trust policy γραμμένο για ref:refs/heads/main σταματά να ταιριάζει.
Για production προτιμάμε scoping ανά environment: εμπιστευόμαστε το repo:acme/api:environment:production και στο GitHub environment ορίζουμε deployment branch rules (μόνο main) και, αν θέλετε, required reviewers. Και το ίδιο το AWS συστήνει έντονα protection rules σε κάθε environment που χρησιμοποιείται έτσι, γιατί το όνομα του environment από μόνο του δεν λέει ποιο branch έτρεξε.
Immutable repository IDs
Το GitHub πλέον εκδίδει sub που περιέχει τα IDs του owner και του repository, στη μορφή repo:acme@ORG-ID/api@REPO-ID:ref:refs/heads/main, για repositories που δημιουργήθηκαν μετά τις 15 Ιουλίου 2026 και για παλαιότερα που το ενεργοποιούν. Αν φτιάξετε νέο repository και αντιγράψετε ένα παλιό trust policy, περιμένετε να αποτύχει. Ελέγξτε ποια μορφή στέλνει το repository σας πριν γράψετε το condition.
Τι να μην κάνετε
Ένα StringLike με repo:acme/* επιτρέπει σε κάθε repository του organisation, ακόμα και σε pull request από οποιοδήποτε branch, να κάνει assume το role. Το IAM ήδη απορρίπτει trust policies για τον GitHub provider όταν το sub λείπει ή είναι σκέτο wildcard, αλλά δέχεται χωρίς πρόβλημα ένα wildcard για όλο το organisation. Οτιδήποτε ευρύτερο από ένα repository, θεωρήστε το εύρημα.
Βήμα 3: least-privilege δικαιώματα στο role
Το deploy role πρέπει να μπορεί να κάνει το deploy και τίποτα άλλο. Για ένα τυπικό pipeline «build image, push στο ECR, update του ECS service», το policy είναι σύντομο:
data "aws_iam_policy_document" "deploy" {
statement {
actions = ["ecr:GetAuthorizationToken"]
resources = ["*"]
}
statement {
actions = [
"ecr:BatchCheckLayerAvailability",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage",
"ecr:BatchGetImage",
]
resources = [aws_ecr_repository.api.arn]
}
statement {
actions = ["ecs:RegisterTaskDefinition", "ecs:DescribeTaskDefinition"]
resources = ["*"]
}
statement {
actions = ["ecs:UpdateService", "ecs:DescribeServices"]
resources = [aws_ecs_service.api.id]
}
statement {
actions = ["iam:PassRole"]
resources = [
aws_iam_role.task_execution.arn,
aws_iam_role.task.arn,
]
condition {
test = "StringEquals"
variable = "iam:PassedToService"
values = ["ecs-tasks.amazonaws.com"]
}
}
}
resource "aws_iam_role_policy" "deploy" {
role = aws_iam_role.github_deploy.id
policy = data.aws_iam_policy_document.deploy.json
}
Δύο συνήθειες κρατούν την κατάσταση διαχειρίσιμη. Ξεχωριστά roles για ξεχωριστές δουλειές: ένα read-only role για terraform plan που μπορούν να το πάρουν τα pull requests, και ένα role με write που το παίρνει μόνο το προστατευμένο environment. Και το iam:PassRole πάντα δεμένο σε συγκεκριμένα roles· ένα ανοιχτό PassRole είναι δρόμος για privilege escalation.
Βήμα 4: το workflow
Το job χρειάζεται id-token: write για να ζητήσει το token, και το action χρειάζεται το ARN του role και ένα region. Η τρέχουσα major έκδοση του action είναι η v6, σύμφωνα με το επίσημο repository.
name: deploy
on:
push:
branches: [main]
permissions:
id-token: write # lets the job request the OIDC token
contents: read # needed by actions/checkout
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v7
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::222222222222:role/github-deploy-api
aws-region: eu-central-1
role-session-name: gh-${{ github.run_id }}
- run: aws sts get-caller-identity
# build, push to ECR, update the ECS service...
Λίγα λόγια για τα inputs:
- Το
audienceέχει defaultsts.amazonaws.com· αφήστε το κενό, εκτός αν δουλεύετε σε non-default partition. - Το
role-duration-secondsέχει default 3600. Αν το ανεβάσετε, ανεβάστε και το maximum session duration του role, αλλιώς το STS θα αρνηθεί. - Με
role-session-nameτο run ID, κάθε εγγραφή στο CloudTrail οδηγεί πίσω σε συγκεκριμένο workflow run. - Για ασφάλεια στο supply chain, κάντε pin τα third-party actions σε πλήρες commit SHA, όπως κάνουν και τα παραδείγματα του GitHub, και αφήστε το Dependabot να τα ενημερώνει.
Όταν ορίζετε permissions σε επίπεδο workflow, όλα τα υπόλοιπα δικαιώματα του GITHUB_TOKEN πέφτουν σε none, που είναι ακριβώς αυτό που θέλετε. Περισσότερα για την υγιεινή των pipelines στο άρθρο μας για τα CI/CD anti-patterns.
Συνηθισμένα errors και πώς τα λύνετε
Σχεδόν κάθε αποτυχία είναι είτε permission που λείπει από το workflow είτε sub claim που δεν ταιριάζει με το trust policy.
«Credentials could not be loaded» / «Could not load credentials from any providers»
Το job δεν πήρε OIDC token, οπότε το action δεν είχε τι να ανταλλάξει. Προσθέστε id-token: write στα permissions του workflow ή του job. Αν ορίσετε permissions σε επίπεδο job, το block του workflow παύει να ισχύει για εκείνο το job. Τα pull requests από forks τρέχουν με read-only permissions και δεν μπορούν να ζητήσουν OIDC token, οπότε μη σχεδιάζετε deploys πάνω τους.
«Not authorized to perform sts:AssumeRoleWithWebIdentity»
Το token ήταν έγκυρο αλλά το trust policy το απέρριψε. Ελέγξτε με αυτή τη σειρά:
- Έχει το job
environment:; Τότε τοsubείναι...:environment:NAME, όχι...:ref:refs/heads/main. - Είναι το trigger
pull_request; Τότε τοsubτελειώνει σε:pull_request. - Είναι νέο repository που στέλνει τη μορφή
org@ID/repo@ID; - Είναι σωστά τα κεφαλαία και μικρά στα ονόματα organisation και repository;
- Βάλατε
StringEqualsμε τιμή που περιέχει*; Τα wildcards θέλουνStringLike.
Για να δείτε τι claims στέλνει πραγματικά το job, κάντε decode το token σε ένα δοκιμαστικό job ή κοιτάξτε το CloudTrail: οι αποτυχημένες κλήσεις AssumeRoleWithWebIdentity καταγράφονται στο account-στόχο.
«No OpenIDConnect provider found in your account»
Το role βρίσκεται σε account που δεν έχει GitHub provider. Ο provider πρέπει να είναι στο ίδιο account με το role που τον εμπιστεύεται.
«The requested DurationSeconds exceeds the MaxSessionDuration»
Ζητήσατε μεγαλύτερο session από όσο επιτρέπει το role. Κατεβάστε το role-duration-seconds ή ανεβάστε το max_session_duration του role.
Το pattern για πολλά accounts
Αν έχετε ξεχωριστά AWS accounts για dev, staging και production (και καλά κάνετε, όπως εξηγούμε στο AWS Organizations από την πρώτη μέρα), βάλτε έναν OIDC provider και ένα deploy role σε κάθε workload account. Κάθε role εμπιστεύεται μόνο το αντίστοιχο GitHub environment.
| Account | Role | sub που εμπιστεύεται |
|---|---|---|
| dev | github-deploy-api | repo:acme/api:environment:dev |
| staging | github-deploy-api | repo:acme/api:environment:staging |
| prod | github-deploy-api | repo:acme/api:environment:production |
Αποθηκεύστε κάθε ARN ως environment variable στο GitHub (όχι secret· ένα ARN δεν είναι ευαίσθητο) και χρησιμοποιήστε το με role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}. Ένα workflow, τρία environments, και ένα παραβιασμένο dev pipeline δεν μπορεί να αγγίξει την production, γιατί το production role δεν θα δεχτεί ποτέ token από dev.
Βάλτε τον provider και το role σε ένα μικρό Terraform module και εφαρμόστε το σε κάθε account μέσα από το baseline stack σας. Αν οργανώνετε το Terraform state ανά account, ταιριάζει φυσικά.
Η εναλλακτική είναι ένα κεντρικό hub role σε shared account που το παίρνει το GitHub και από εκεί κάνει chaining στα roles κάθε workload account. Δουλεύει (το action το υποστηρίζει με role-chaining: true, και τα roles-στόχοι πρέπει να επιτρέπουν sts:TagSession ή να ορίσετε role-skip-session-tagging), αλλά από την εμπειρία μας προσθέτει ένα single point of failure και κάνει τα trust policies πιο δυσανάγνωστα. Τα απευθείας roles ανά account ελέγχονται πολύ πιο εύκολα.
Checklist μετάβασης
- Βρείτε κάθε IAM user με ενεργά access keys (
aws iam generate-credential-report) και εντοπίστε όσα χρησιμοποιεί το CI. - Φτιάξτε τον provider και ένα scoped role ανά pipeline και account.
- Γυρίστε ένα workflow, επιβεβαιώστε στο CloudTrail ότι χρησιμοποιείται το role και μετά απενεργοποιήστε το παλιό key.
- Μετά από μια εβδομάδα χωρίς χρήση, σβήστε το key και τον IAM user, και αφαιρέστε τα GitHub secrets.
- Βάλτε ένα guardrail ώστε κάθε νέο access key να βγάζει alert, για παράδειγμα με ένα AWS Config rule ή ένα Security Hub control.
Η μετάβαση του CI από στατικά keys σε OIDC είναι από τα πρώτα πράγματα που κάνουμε όταν στήνουμε τα cloud foundations μιας ομάδας, μαζί με το SSO για τους ανθρώπους (δείτε τον οδηγό μας για το στήσιμο του IAM Identity Center). Αν θέλετε να γίνει σωστά σε όλα τα accounts και τα pipelines σας, αυτό ακριβώς καλύπτει η δουλειά μας σε cloud migration και foundations.
Συχνές ερωτήσεις
Χρειάζεται ακόμα thumbprint για τον GitHub OIDC provider στο AWS;
Όχι. Το AWS επαληθεύει το endpoint υπογραφής των tokens του GitHub με τη λίστα του από trusted root certificate authorities, οπότε το thumbprint δεν χρησιμοποιείται για το GitHub. Στο Terraform το thumbprint_list είναι προαιρετικό, και η τεκμηρίωση του configure-aws-credentials αναφέρει ότι όποιο thumbprint ορίσετε αγνοείται.
Γιατί το GitHub Actions βγάζει «Not authorized to perform sts:AssumeRoleWithWebIdentity»;
Το sub condition του trust policy δεν ταιριάζει με το token. Η πιο συχνή αιτία είναι ένα job με environment:, που αλλάζει το sub σε repo:ORG/REPO:environment:NAME. Τα pull requests δίνουν :pull_request, και τα νέα repositories μπορεί να στέλνουν τη μορφή org@ID/repo@ID.
Ποια έκδοση του aws-actions/configure-aws-credentials να χρησιμοποιήσω;
Τον Οκτώβριο του 2026 η τρέχουσα major έκδοση είναι η v6. Χρησιμοποιήστε @v6 ή, καλύτερα, κάντε pin σε πλήρες commit SHA και αφήστε το Dependabot να το ενημερώνει. Δώστε role-to-assume και aws-region, και βεβαιωθείτε ότι το workflow έχει id-token: write.
Μπορεί ένα GitHub workflow να κάνει deploy σε πολλά AWS accounts με OIDC;
Ναι. Φτιάξτε OIDC provider και deploy role σε κάθε account, κάθε role να εμπιστεύεται ένα συγκεκριμένο GitHub environment, και αποθηκεύστε το ARN ως environment variable. Το ίδιο workflow παίρνει έτσι το σωστό role ανά environment, και η production δέχεται tokens μόνο από το production environment.