Skip to content
Open
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
10 changes: 8 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ GEM
base64 (0.2.0)
bcrypt_pbkdf (1.1.1)
bcrypt_pbkdf (1.1.1-arm64-darwin)
bcrypt_pbkdf (1.1.1-x86_64-darwin)
benchmark (0.4.0)
bigdecimal (3.1.9)
builder (3.3.0)
Expand All @@ -97,6 +98,7 @@ GEM
erb (5.0.1)
erubi (1.13.1)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
globalid (1.2.1)
activesupport (>= 6.1)
Expand Down Expand Up @@ -142,9 +144,11 @@ GEM
net-protocol
net-ssh (7.2.3)
nio4r (2.7.4)
nokogiri (1.18.8-arm64-darwin)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-gnu)
nokogiri (1.18.10-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
parallel (1.24.0)
parser (3.3.1.0)
Expand Down Expand Up @@ -244,6 +248,7 @@ GEM
logger
securerandom (0.4.1)
sqlite3 (2.6.0-arm64-darwin)
sqlite3 (2.6.0-x86_64-darwin)
sqlite3 (2.6.0-x86_64-linux-gnu)
stringio (3.1.7)
strscan (3.1.0)
Expand All @@ -262,6 +267,7 @@ GEM

PLATFORMS
arm64-darwin-24
x86_64-darwin-23
x86_64-linux

DEPENDENCIES
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ AzureBlob supports managed identities on :
- App Service
- Azure Functions (Untested but should work)
- Azure Containers (Untested but should work)
- Azure AD Workload Identity (AKS/K8s)

AKS support will likely require more work. Contributions are welcome.

Expand All @@ -46,6 +47,21 @@ prod:
principal_id: 71b34410-4c50-451d-b456-95ead1b18cce
```

#### Azure AD Workload Identity (AKS/K8s)

ActiveStorage config example:

```
prod:
service: AzureBlob
container: container_name
storage_account_name: account_name
use_managed_identities: true
```

> uses `AZURE_CLIENT_ID`, `AZURE_TENANT_ID` and `AZURE_FEDERATED_TOKEN_FILE` environment variables, made available by AKS cluster when Azure AD Workload Identity is set up properly.


### Azurite

To use Azurite, pass the `storage_blob_host` config key with the Azurite URL (`http://127.0.0.1:10000/devstoreaccount1` by default)
Expand Down
23 changes: 7 additions & 16 deletions lib/azure_blob/identity_token.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
require_relative "instance_metadata_service"
require_relative "workload_identity"
require "json"

module AzureBlob
class IdentityToken
RESOURCE_URI = "https://storage.azure.com/"
EXPIRATION_BUFFER = 600 # 10 minutes

IDENTITY_ENDPOINT = ENV["IDENTITY_ENDPOINT"] || "http://169.254.169.254/metadata/identity/oauth2/token"
API_VERSION = ENV["IDENTITY_ENDPOINT"] ? "2019-08-01" : "2018-02-01"

def initialize(principal_id: nil)
@identity_uri = URI.parse(IDENTITY_ENDPOINT)
params = {
'api-version': API_VERSION,
resource: RESOURCE_URI,
}
params[:principal_id] = principal_id if principal_id
@identity_uri.query = URI.encode_www_form(params)
@service = AzureBlob::WorkloadIdentity.federated_token? ?
AzureBlob::WorkloadIdentity.new : AzureBlob::InstanceMetadataService.new(principal_id: principal_id)
end

def to_s
Expand All @@ -31,13 +24,11 @@ def expired?

def refresh
return unless expired?
headers = { "Metadata" => "true" }
headers["X-IDENTITY-HEADER"] = ENV["IDENTITY_HEADER"] if ENV["IDENTITY_HEADER"]

attempt = 0
begin
attempt += 1
response = JSON.parse(AzureBlob::Http.new(identity_uri, headers).get)
response = JSON.parse(service.request)
rescue AzureBlob::Http::Error => error
if should_retry?(error, attempt)
attempt = 1 if error.status == 410
Expand All @@ -48,7 +39,7 @@ def refresh
raise
end
@token = response["access_token"]
@expiration = Time.at(response["expires_on"].to_i)
@expiration = Time.at((response["expires_on"] || response["expires_in"]).to_i)
end

def should_retry?(error, attempt)
Expand All @@ -61,6 +52,6 @@ def exponential_backoff(error, attempt)
end
EXPONENTIAL_BACKOFF = [ 2, 6, 14, 30 ]

attr_reader :identity_uri, :expiration, :token
attr_reader :service, :expiration, :token
end
end
24 changes: 24 additions & 0 deletions lib/azure_blob/instance_metadata_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module AzureBlob
class InstanceMetadataService # Azure Instance Metadata Service (IMDS)
IDENTITY_ENDPOINT = ENV["IDENTITY_ENDPOINT"] || "http://169.254.169.254/metadata/identity/oauth2/token"
API_VERSION = ENV["IDENTITY_ENDPOINT"] ? "2019-08-01" : "2018-02-01"
RESOURCE_URI = "https://storage.azure.com/"

def initialize(principal_id: nil)
@identity_uri = URI.parse(IDENTITY_ENDPOINT)
params = {
'api-version': API_VERSION,
resource: AzureBlob::IdentityToken::RESOURCE_URI
}
params[:principal_id] = principal_id if principal_id
@identity_uri.query = URI.encode_www_form(params)
end

def request
headers = { "Metadata" => "true" }
headers["X-IDENTITY-HEADER"] = ENV["IDENTITY_HEADER"] if ENV["IDENTITY_HEADER"]

AzureBlob::Http.new(@identity_uri, headers).get
end
end
end
33 changes: 33 additions & 0 deletions lib/azure_blob/workload_identity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module AzureBlob
class WorkloadIdentity # Azure AD Workload Identity
IDENTITY_ENDPOINT = "https://login.microsoftonline.com/#{ENV['AZURE_TENANT_ID']}/oauth2/v2.0/token"
CLIENT_ID = ENV['AZURE_CLIENT_ID']
SCOPE = "https://storage.azure.com/.default"
GRANT_TYPE = "client_credentials"
CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"

FEDERATED_TOKEN_FILE = ENV["AZURE_FEDERATED_TOKEN_FILE"].to_s

def self.federated_token?
!FEDERATED_TOKEN_FILE.empty?
end

def request
AzureBlob::Http.new(URI.parse(IDENTITY_ENDPOINT)).post(
URI.encode_www_form(
client_id: CLIENT_ID,
scope: SCOPE,
client_assertion_type: CLIENT_ASSERTION_TYPE,
client_assertion: federated_token,
grant_type: GRANT_TYPE
)
)
end

private

def federated_token
File.read(FEDERATED_TOKEN_FILE).strip
end
end
end