Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,6 @@ jobs:
draft: false
prerelease: true
files: |
build/bin/jfrog-credential-provider-aws-linux-amd64
build/bin/jfrog-credential-provider-aws-linux-arm64
build/bin/jfrog-credential-provider-linux-amd64
build/bin/jfrog-credential-provider-linux-arm64
fail_on_unmatched_files: true
12 changes: 6 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@ jobs:
- name: Sign Binaries
run: |
cd build/bin/
echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --detach-sign --armor jfrog-credential-provider-aws-linux-amd64
echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --detach-sign --armor jfrog-credential-provider-aws-linux-arm64
echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --detach-sign --armor jfrog-credential-provider-linux-amd64
echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --detach-sign --armor jfrog-credential-provider-linux-arm64

- name: Upload release artifacts
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.inputs.tag }}
generate_release_notes: true
files: |
build/bin/jfrog-credential-provider-aws-linux-amd64
build/bin/jfrog-credential-provider-aws-linux-arm64
build/bin/jfrog-credential-provider-aws-linux-amd64.asc
build/bin/jfrog-credential-provider-aws-linux-arm64.asc
build/bin/jfrog-credential-provider-linux-amd64
build/bin/jfrog-credential-provider-linux-arm64
build/bin/jfrog-credential-provider-linux-amd64.asc
build/bin/jfrog-credential-provider-linux-arm64.asc

100 changes: 91 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
JFROG_CREDENTIAL_PLUGIN_BINARY_URL:
description: 'BINARY_URL (CI adds arch suffix automatically)'
required: true
default: "https://releases.jfrog.io/artifactory/run/jfrog-credentials-provider/0.1.0-beta.1/jfrog-credential-provider-aws-linux"
default: "https://partnership.jfrog.io/artifactory/credential-provider-test/jfrog-credential-provider"
type: string
DISABLE_TERRAFORM_DESTROY:
description: 'DISABLE_TERRAFORM_DESTROY'
Expand All @@ -22,7 +22,7 @@ env:
TF_VERSION: 1.5.7

jobs:
verify-kubelet-plugin:
verify-kubelet-plugin-aws:
runs-on: self-hosted
env:
ARTIFACTORY_TOKEN: ${{ secrets.ARTIFACTORY_TOKEN }}
Expand All @@ -42,6 +42,13 @@ jobs:
run: |
aws sts get-caller-identity

- name: Login to Azure with Federated Credentials
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_APP_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_APP_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_APP_SUBSCRIPTION_ID }}

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
Expand All @@ -50,22 +57,97 @@ jobs:

- name: Initialise Terraform
id: init
env:
AZURE_APP_SUBSCRIPTION_ID: ${{ secrets.AZURE_APP_SUBSCRIPTION_ID }}
run: |
echo "" >> build/terraform.tfvars
echo "jfrog_credential_provider_binary_url=\"$JFROG_CREDENTIAL_PLUGIN_BINARY_URL\"" >> build/terraform.tfvars
cp build/terraform.tfvars terraform-ci/terraform.tfvars
echo "" >> build/terraform.tfvars.aws
echo "jfrog_credential_provider_binary_url=\"$JFROG_CREDENTIAL_PLUGIN_BINARY_URL\"" >> build/terraform.tfvars.aws
# for azure, it is not possible to avoid azure authentication check, even when azure is disabled
echo "azure_subscription_id=\"$AZURE_APP_SUBSCRIPTION_ID\"" >> build/terraform.tfvars.aws
cp build/terraform.tfvars.aws terraform-ci/terraform.tfvars
cd terraform-ci
terraform init

- name: Run Terraform CI
- name: Run AWS Terraform CI
id: apply
run: |
cd terraform-ci
terraform apply -input=false -auto-approve
terraform output -json > terraform_output.json
echo "Terraform output: $(cat terraform_output.json)"

- name: Destroy AWS terraform resources
id: destroy
if: always() && !env.DISABLE_TERRAFORM_DESTROY
continue-on-error: true
run: |
cd terraform-ci
terraform destroy -input=false -auto-approve
rm terraform.tfstate terraform.tfstate.backup terraform_output.json

- name: Upload Terraform context for manual cleanup
if: always()
uses: actions/upload-artifact@v4
with:
name: terraform-context-for-manual-cleanup-aws
path: |
terraform-ci/**/*.tf
terraform-ci/jfrog/*
terraform-ci/terraform.tfstate
terraform-ci/terraform.tfstate.backup
terraform-ci/terraform.tfvars
terraform-ci/.terraform.lock.hcl
terraform-ci/terraform_output.json
retention-days: 1

verify-kubelet-plugin-azure:
runs-on: self-hosted
env:
ARTIFACTORY_TOKEN: ${{ secrets.ARTIFACTORY_TOKEN }}
JFROG_CREDENTIAL_PLUGIN_BINARY_URL: ${{ github.event.inputs.JFROG_CREDENTIAL_PLUGIN_BINARY_URL }}
steps:
- name: Checkout
uses: actions/checkout@v2

- name: Install Azure CLI
uses: pietrobolcato/install-azure-cli-action@main

- name: Login to Azure with Federated Credentials
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_APP_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_APP_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_APP_SUBSCRIPTION_ID }}

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
terraform_wrapper: false

- name: Initialise Terraform
id: init
env:
AZURE_APP_SUBSCRIPTION_ID: ${{ secrets.AZURE_APP_SUBSCRIPTION_ID }}
run: |
echo "" >> build/terraform.tfvars.azure
echo "jfrog_credential_provider_binary_url=\"$JFROG_CREDENTIAL_PLUGIN_BINARY_URL\"" >> build/terraform.tfvars.azure
echo "azure_subscription_id=\"$AZURE_APP_SUBSCRIPTION_ID\"" >> build/terraform.tfvars.azure
cp build/terraform.tfvars.azure terraform-ci/terraform.tfvars
cd terraform-ci
terraform init

- name: Run Azure Terraform CI
id: apply
run: |
# to avoid credentials check for aws
cd terraform-ci
cat terraform.tfvars
terraform apply -input=false -auto-approve
terraform output -json > terraform_output.json
echo "Terraform output: $(cat terraform_output.json)"

- name: Destroy terraform resources
- name: Destroy Azure terraform resources
id: destroy
if: always() && !env.DISABLE_TERRAFORM_DESTROY
continue-on-error: true
Expand All @@ -78,7 +160,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v4
with:
name: terraform-context-for-manual-cleanup
name: terraform-context-for-manual-cleanup-azure
path: |
terraform-ci/**/*.tf
terraform-ci/jfrog/*
Expand All @@ -87,4 +169,4 @@ jobs:
terraform-ci/terraform.tfvars
terraform-ci/.terraform.lock.hcl
terraform-ci/terraform_output.json
retention-days: 1
retention-days: 1
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ This project is currently in its beta phase, meaning it's still under active dev

# JFrog Kubelet Credential Provider

A [Kubernetes kubelet credential provider](https://kubernetes.io/docs/tasks/administer-cluster/kubelet-credential-provider/) **for Amazon EKS** that enables seamless authentication with JFrog Artifactory for container image pulls in Amazon EKS, eliminating the need for manual image pull secret management.
A [Kubernetes kubelet credential provider](https://kubernetes.io/docs/tasks/administer-cluster/kubelet-credential-provider/) **for Amazon EKS and Azure AKS** that enables seamless authentication with JFrog Artifactory for container image pulls, eliminating the need for manual image pull secret management.

> **Coming Soon**: Azure AKS and Google Cloud GKE support are currently in development.
> **Coming Soon**: Google Cloud GKE support is currently in development.

## Overview

Expand All @@ -22,7 +22,7 @@ The JFrog Kubelet Credential Provider leverages the native Kubernetes kubelet Cr
1. A pod is created with an image stored in JFrog Artifactory
2. Kubelet identifies the image URL matches the configured pattern for the JFrog Kubelet Credential Provider
3. Kubelet invokes the JFrog Kubelet Credential Provider binary
4. The provider authenticates with AWS (using IAM roles or OIDC) and exchanges credentials with Artifactory
4. The provider authenticates with the cloud provider (AWS IAM roles/OIDC or Azure managed identities) and exchanges credentials with Artifactory
5. Valid registry credentials are returned to kubelet for the image pull

## Quick Start
Expand Down Expand Up @@ -51,18 +51,26 @@ See the [terraform-module](./terraform-module) directory for detailed deployment

## Authentication Methods

### AWS Authentication
- **AWS IAM Role Assumption**: Uses EC2 instance [IAM roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) for authentication
- **AWS Cognito OIDC**: Uses OIDC tokens from [AWS Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html) for authentication

**Note**: You must select either IAM Role Assumption OR Cognito OIDC as your authentication method. They cannot be used simultaneously in the same deployment.
**Note**: For AWS, You must select either IAM Role Assumption OR Cognito OIDC as your authentication method.
They cannot be used simultaneously in the same deployment.

### Azure Authentication
- **Azure Managed Identity OIDC**: Uses [Azure managed identities](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) with OIDC for authentication


## Requirements

- Amazon EKS cluster
- Amazon EKS cluster or Azure AKS cluster
- JFrog Artifactory instance
- Based on your chosen authentication method:
- **For IAM Role Assumption**: IAM role mapped to a JFrog Artifactory user
- **For Cognito OIDC**: OIDC provider and identity mappings. For more information, see [terraform-module](./terraform-module)
- Based on your chosen cloud provider and authentication method:
- **For AWS IAM Role Assumption**: IAM role mapped to a JFrog Artifactory user
- **For AWS Cognito OIDC**: OIDC provider and identity mappings
- **For Azure Managed Identity**: Azure AD application for OIDC and kubelet Identity.
For more information, see [terraform-module](./terraform-module)


## Logging and Debugging
Expand Down
5 changes: 4 additions & 1 deletion build/terraform.tfvars → build/terraform.tfvars.aws
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ self_managed_eks_cluster = {
name = "aws-operator-jfrog"
}

jfrog_namespace = "jfrog"
jfrog_namespace = "jfrog"

enable_aws = true

42 changes: 42 additions & 0 deletions build/terraform.tfvars.azure
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
region = "ap-northeast-3"

# The JFrog Credential Provider binary URL (no authentication required)
# added by CI
# jfrog_credential_provider_binary_url = "https://releases.jfrog.io/artifactory/run/jfrog-credentials-provider/0.1.0-beta.1/jfrog-credential-provider-aws-linux"

# The JFrog Artifactory URL (the one that will be the EKS container registry)
artifactory_url = "partnership.jfrog.io"
# Change this to jfrogurl


# The JFrog Artifactory username that will be granted the assume role permission
artifactory_user = "aws-eks-user"

create_eks_cluster = false
# cluster_public_access_cidrs = ["0.0.0.0/0"]
# cluster_name = "demo-eks-cluster"

self_managed_eks_cluster = {
name = "aws-operator-jfrog"
}

jfrog_namespace = "jfrog"

enable_aws = false

enable_azure = true

azure_resource_group_name = "infra-robin-test"

azure_location = "eastus"

create_aks_cluster = true

aks_cluster_name = "jfrog-test-aks"

azure_node_count = 3
azure_node_vm_size = "Standard_D2pds_v5"
azure_admin_username = "jfrog"

# time to live is very short for testing
azure_cluster_public_access_cidrs = ["0.0.0.0/0"]
2 changes: 1 addition & 1 deletion internal/autoupdate/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func downloadLatestBinary(ctx context.Context, logs *logger.Logger, client *http
logs.Info("Release tag '%s' is missing 'v' prefix. Prepending 'v'." + newVersion)
newVersion = strings.TrimPrefix(newVersion, "v")
}
downloadUrl := jfrogPluginDownloadUrl + downloadSuffix + newVersion + "/jfrog-credential-provider-aws-linux-" + getArchSuffix(logs)
downloadUrl := jfrogPluginDownloadUrl + downloadSuffix + newVersion + "/jfrog-credential-provider-linux-" + getArchSuffix(logs)
logs.Info("Downloading new binary from: " + downloadUrl)
downloadSignUrl := downloadUrl + ".asc"
err := downloadReleaseArtifacts(ctx, logs, client, newBinaryPath, downloadUrl)
Expand Down
35 changes: 28 additions & 7 deletions internal/handlers/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const (
REGION_URL = "http://169.254.169.254/latest/meta-data/placement/region"
GRANT_TYPE = "client_credentials"
AWS_OIDC_TOKEN_URL = "https://$user_pool_resource_domain.auth.$region.amazoncognito.com/oauth2/token"
METADATA_URL = "http://169.254.169.254/latest/meta-data/"
)

type AwsOidcResult struct {
Expand Down Expand Up @@ -180,12 +181,12 @@ func GetAWSSignedRequest(s *service.Service, ctx context.Context, awsRoleName st
// get token from metadata service
token, err := getToken(s, ctx)
if err != nil {
return nil, fmt.Errorf("Error getting aws token, ", err)
return nil, fmt.Errorf("Error getting aws token, %v", err)
}
// get temp credentials from metadata service
tempCredentials, err := getTempCredentials(s, ctx, token, awsRoleName)
if err != nil {
return nil, fmt.Errorf("GetTempCredentials returned err ", err)
return nil, fmt.Errorf("GetTempCredentials returned err %v", err)
}
s.Logger.Info("GetTempCredentials returned code :" + tempCredentials.Code)
if tempCredentials.Code != "Success" {
Expand Down Expand Up @@ -282,14 +283,14 @@ func getAWSRegion(s *service.Service, ctx context.Context, token string) (string
// Then get the region
req, err := http.NewRequestWithContext(ctx, "GET", REGION_URL, nil)
if err != nil {
return "", fmt.Errorf("Error creating request to get AWS region:", err)
return "", fmt.Errorf("Error creating request to get AWS region: %v", err)
}

req.Header.Add("X-aws-ec2-metadata-token", token)

resp, err := s.Client.Do(req)
if err != nil {
return "", fmt.Errorf("Error getting AWS region:", err)
return "", fmt.Errorf("Error getting AWS region: %v", err)
}
defer resp.Body.Close()

Expand Down Expand Up @@ -346,7 +347,7 @@ func getResourceServerId(s *service.Service, cfg aws.Config, userPoolName string
cognitoSvc := cognitoidentityprovider.NewFromConfig(cfg)
userPoolId, err := getUserPoolId(s, cfg, cognitoSvc, userPoolName)
if err != nil {
return "", "", fmt.Errorf("failed to get user pool id:", err)
return "", "", fmt.Errorf("failed to get user pool id: %v", err)
}
s.Logger.Info("user pool id :" + userPoolId)
// Retrieve detailed information about the user pool
Expand All @@ -356,7 +357,7 @@ func getResourceServerId(s *service.Service, cfg aws.Config, userPoolName string
// Retrieve detailed information about the user pool
userPoolResult, err := cognitoSvc.DescribeUserPool(context.TODO(), describeInput)
if err != nil {
return "", "", fmt.Errorf("failed to describe user pool:", err)
return "", "", fmt.Errorf("failed to describe user pool: %v", err)
}
// Print the User Pool Domain
if userPoolResult.UserPool.Domain != nil {
Expand All @@ -377,7 +378,7 @@ func getResourceServerId(s *service.Service, cfg aws.Config, userPoolName string

result, err := cognitoSvc.ListResourceServers(context.TODO(), input)
if err != nil {
return "", "", fmt.Errorf("failed to list user pool resource servers:", err)
return "", "", fmt.Errorf("failed to list user pool resource servers: %v", err)
}

// Check if any user pools are present
Expand All @@ -396,3 +397,23 @@ func getResourceServerId(s *service.Service, cfg aws.Config, userPoolName string
}
return "", "", fmt.Errorf("resource Server not found")
}

func CheckIfAWS(s *service.Service, ctx context.Context) (bool, error) {
s.Logger.Info("Checking if cloud provider is AWS")
token, err := getToken(s, ctx)
if err != nil {
return false, fmt.Errorf("Error getting aws token: %v", err)
}
req, err := http.NewRequestWithContext(ctx, "GET", METADATA_URL, nil)
req.Header.Add("X-aws-ec2-metadata-token", token)
if err != nil {
return false, fmt.Errorf("Error creating request to check if cloud provider is AWS: %v", err)
}
resp, err := s.Client.Do(req)
if err != nil {
return false, fmt.Errorf("Error checking if cloud provider is AWS: %v", err)
}
defer resp.Body.Close()
s.Logger.Info(fmt.Sprintf("AWS metadata server response status code: %d", resp.StatusCode))
return (resp.StatusCode == http.StatusOK), nil
}
Loading