mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-08-23 12:24:57 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a1cee7860 |
||
|
|
b90743885a |
||
|
|
7b56989b51 |
8 changed files with 365 additions and 43 deletions
|
|
@ -120,6 +120,7 @@ upstream:
|
||||||
# - bearer: Authorization header with Bearer token
|
# - bearer: Authorization header with Bearer token
|
||||||
# - basic: Authorization header with Basic auth (username:password)
|
# - basic: Authorization header with Basic auth (username:password)
|
||||||
# - header: Custom header name and value
|
# - header: Custom header name and value
|
||||||
|
# - ecr: AWS ECR auto-refreshing token via the AWS SDK credential chain
|
||||||
auth:
|
auth:
|
||||||
# Example: npm with bearer token
|
# Example: npm with bearer token
|
||||||
# "https://registry.npmjs.org":
|
# "https://registry.npmjs.org":
|
||||||
|
|
@ -143,6 +144,13 @@ upstream:
|
||||||
# header_name: "X-Auth-Token"
|
# header_name: "X-Auth-Token"
|
||||||
# header_value: "${MAVEN_TOKEN}"
|
# header_value: "${MAVEN_TOKEN}"
|
||||||
|
|
||||||
|
# Example: private AWS ECR registry (12h tokens auto-refreshed via
|
||||||
|
# ecr:GetAuthorizationToken; credentials come from the AWS SDK default
|
||||||
|
# chain, so IRSA / instance profiles / AWS_* env vars all work)
|
||||||
|
# "https://123456789012.dkr.ecr.eu-west-1.amazonaws.com":
|
||||||
|
# type: ecr
|
||||||
|
# region: eu-west-1
|
||||||
|
|
||||||
# Gradle HttpBuildCache configuration
|
# Gradle HttpBuildCache configuration
|
||||||
gradle:
|
gradle:
|
||||||
build_cache:
|
build_cache:
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,22 @@ upstream:
|
||||||
header_value: "${MAVEN_TOKEN}"
|
header_value: "${MAVEN_TOKEN}"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### AWS ECR
|
||||||
|
|
||||||
|
Private ECR registries issue authorization tokens that expire after 12 hours. The `ecr` auth type calls `ecr:GetAuthorizationToken` on demand, caches the result, and refreshes it shortly before expiry, so no static credential appears in the config file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
upstream:
|
||||||
|
oci:
|
||||||
|
ecr: "https://123456789012.dkr.ecr.eu-west-1.amazonaws.com"
|
||||||
|
auth:
|
||||||
|
"https://123456789012.dkr.ecr.eu-west-1.amazonaws.com":
|
||||||
|
type: ecr
|
||||||
|
region: eu-west-1
|
||||||
|
```
|
||||||
|
|
||||||
|
AWS credentials are resolved by the SDK's default chain, which covers EKS IAM Roles for Service Accounts (IRSA), EC2/ECS instance profiles, `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` environment variables, and `~/.aws/credentials`. The IAM identity needs the `ecr:GetAuthorizationToken` action plus the usual `ecr:BatchGetImage` / `ecr:GetDownloadUrlForLayer` permissions on the target repositories. If `region` is omitted the SDK's default region resolution applies.
|
||||||
|
|
||||||
### URL Matching
|
### URL Matching
|
||||||
|
|
||||||
Auth keys must be absolute URLs. Matching compares the scheme, host, effective port, and path-segment prefix, preventing credentials for `registry.example.com` from being sent to a lookalike host such as `registry.example.com.evil.test`. The longest matching scope wins, so you can configure different credentials for different paths:
|
Auth keys must be absolute URLs. Matching compares the scheme, host, effective port, and path-segment prefix, preventing credentials for `registry.example.com` from being sent to a lookalike host such as `registry.example.com.evil.test`. The longest matching scope wins, so you can configure different credentials for different paths:
|
||||||
|
|
|
||||||
29
go.mod
29
go.mod
|
|
@ -7,6 +7,8 @@ toolchain go1.26.6
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
github.com/CycloneDX/cyclonedx-go v0.11.0
|
github.com/CycloneDX/cyclonedx-go v0.11.0
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.38
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ecr v1.60.7
|
||||||
github.com/git-pkgs/archives v0.5.1
|
github.com/git-pkgs/archives v0.5.1
|
||||||
github.com/git-pkgs/cooldown v0.1.1
|
github.com/git-pkgs/cooldown v0.1.1
|
||||||
github.com/git-pkgs/enrichment v0.6.5
|
github.com/git-pkgs/enrichment v0.6.5
|
||||||
|
|
@ -74,25 +76,24 @@ require (
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||||
github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
|
github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
|
||||||
github.com/ashanbrown/makezero/v2 v2.2.1 // indirect
|
github.com/ashanbrown/makezero/v2 v2.2.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.9 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.43.7 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.20 // indirect
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.37 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.19 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.3 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.38 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.38 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.39 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.38 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 // indirect
|
github.com/aws/aws-sdk-go-v2/service/signin v1.5.7 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.33.7 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.7 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.45.7 // indirect
|
||||||
github.com/aws/smithy-go v1.26.0 // indirect
|
github.com/aws/smithy-go v1.27.8 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bkielbasa/cyclop v1.2.3 // indirect
|
github.com/bkielbasa/cyclop v1.2.3 // indirect
|
||||||
github.com/blizzy78/varnamelen v0.8.0 // indirect
|
github.com/blizzy78/varnamelen v0.8.0 // indirect
|
||||||
|
|
|
||||||
58
go.sum
58
go.sum
|
|
@ -117,44 +117,46 @@ github.com/ashanbrown/forbidigo/v2 v2.3.1 h1:KAZijvQ7zeIBKbhikT4jCm0TLYXC4u78bTi
|
||||||
github.com/ashanbrown/forbidigo/v2 v2.3.1/go.mod h1:2QDkLTzU6TV937eFROamXrW92M3paehdae4HCDCOZCM=
|
github.com/ashanbrown/forbidigo/v2 v2.3.1/go.mod h1:2QDkLTzU6TV937eFROamXrW92M3paehdae4HCDCOZCM=
|
||||||
github.com/ashanbrown/makezero/v2 v2.2.1 h1:A7uU8dgB1PA9aelTxHMfHIQ8Qev8AB3JLxJUBUsejqM=
|
github.com/ashanbrown/makezero/v2 v2.2.1 h1:A7uU8dgB1PA9aelTxHMfHIQ8Qev8AB3JLxJUBUsejqM=
|
||||||
github.com/ashanbrown/makezero/v2 v2.2.1/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY=
|
github.com/ashanbrown/makezero/v2 v2.2.1/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.9 h1:/rYeyO2+HrMztAmxAq9++XJtFMqSIpSsNA0yDGALYq4=
|
github.com/aws/aws-sdk-go-v2 v1.43.7 h1:msCzvkeYJA9ehbV8mRRmkZLo/zJg/+yDVLNtflg83hQ=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.9/go.mod h1:+HsoOEX80qAVUitj1A2DhCNTjmb3edVyuDypb6LNEeo=
|
github.com/aws/aws-sdk-go-v2 v1.43.7/go.mod h1:tXpPM+v0D1lndmga+HqqLDIzUFJlEeR21aspVklHF00=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 h1:h5+3VT69KUBK24grGuuA5saDJTj2IIjLb9au668Fo5I=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 h1:h5+3VT69KUBK24grGuuA5saDJTj2IIjLb9au668Fo5I=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11/go.mod h1:dnakxebH6UwFvcvujL0LVggYQ8nEvBGjU4G/V79Nv94=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11/go.mod h1:dnakxebH6UwFvcvujL0LVggYQ8nEvBGjU4G/V79Nv94=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.20 h1:8VMDnWc/kEzxsI/1ngGM9mG81a8IGmIHD8KLcYGwagc=
|
github.com/aws/aws-sdk-go-v2/config v1.32.38 h1:n4yPHBjtQ3BrIIUyk0/LAqf/BL2iv0Tw6XZcMRzM0ps=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.20/go.mod h1:PuwEpciweIXGULWeOeSTXtSbH4CW9mWdWrhdCKQI1sM=
|
github.com/aws/aws-sdk-go-v2/config v1.32.38/go.mod h1:dencYsOS1R7rBy8zehCvwBYzdxxL4Q/nRK7In03wjN8=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.19 h1:yuFzSV1U0aRNYCQGVaTY2zW2M/L93pYHnXnrJUphYhU=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.37 h1:FJ8Iz4/xISMB/rwLlgfWujfGDFWr0oneQgtA6KPcYLY=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.19/go.mod h1:7y63L1kGzeoDlJaQ3Z578KrnmfBut96JjvJUzGwR+YE=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.37/go.mod h1:Q6pWOgVUp49x4g5QVi29wHofUoICnZ+Zq4jHbRN/7ec=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 h1:0w6dCiO8iez+YKwRhRBlL1CH/E3GTfdkuzrwj1by8vo=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38 h1:Nqo2jU1wz5rnBM9XQyXfVD1RP8txkbP3EDx8hR/hbCE=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25/go.mod h1:9FDWUothyr5RCRAHc45XOiVCzUR8n/IhCYX+uVqw6vk=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38/go.mod h1:PzJFHhjR2vWFKHe8HmY5Lxhvwyxnr5MERtk0nDxWNbk=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.3 h1:w5OoDiMN6x53ROmiIImGzmVcxXv2q1GXY+aKV4WAJYM=
|
github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.3 h1:w5OoDiMN6x53ROmiIImGzmVcxXv2q1GXY+aKV4WAJYM=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.3/go.mod h1:dAhgYp776bX3LuWvnSCFwQEjNs6fuFg7YXIy5PXcP3Q=
|
github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.3/go.mod h1:dAhgYp776bX3LuWvnSCFwQEjNs6fuFg7YXIy5PXcP3Q=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 h1:Uii3frf9ztec/ABM2/FSH9/z7PLzxfpG8h4RpkUFflQ=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.38 h1:MBMg0zJ6i4TkAJ0dVFLKKn2cOkY6FkicmUDM67BRr6g=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25/go.mod h1:G6kntsA2GorAxDPbap6xgB2F+amSLUF8GJTi7PUoX44=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.38/go.mod h1:9MWuJbyiUyj6eA7W1/zm1zuePDPSB3g+xcgRQeMWsXc=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 h1:r1+/l6m+WaUJF9HISEsNOLHSNj5EXYQxK8VX6Cz9NlA=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.38 h1:lHm4jPf3k1Lz5ZWc+Vcn3MKVwym+26kWCba9FkJ4f0Y=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25/go.mod h1:cKf+D+NMDK1LndD7BowHbBZPgR9V0/5HubH0PFWvA+c=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.38/go.mod h1:Rn+P2XR+FbyZzjmWKjg/KUZNxmGfr5oZwh5jQiE+CzI=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 h1:A1PmWU2zfkIm9EyFlJncFXL4W4phML+h8KjltUsCvNQ=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.39 h1:vo4xvMRs/F6h1E52qsgLqCQgWIQXgIJUauG6rlZEh4U=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26/go.mod h1:dY4MRzXEizrD4hqtpKvWVGPX7QleSGGVY+EBolo1RmM=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.39/go.mod h1:jB03R1ij/A+OE2e1dz6vgj076gd7vlYcfstAzj3HcnU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 h1:d5/908OJ4bXg8lyjeMPvXetEKqoDoLi5Owy1zNue3yg=
|
github.com/aws/aws-sdk-go-v2/service/ecr v1.60.7 h1:HgGvHNphJn16nP+JI6zzhYk1Y6f3BeDqbc7EYgpYKxU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10/go.mod h1:a57l7Hwh+FWI+we50g5NPJHYUKeJKfXbc4w8SyXu8Ig=
|
github.com/aws/aws-sdk-go-v2/service/ecr v1.60.7/go.mod h1:dCl1nYmyWmxJO6mBprZdwoFbIBEieBSifLNAt1xnSe4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 h1:OvYZOB3qA6zvfdRFiRFRzVSiElMYrz3GdntkXZxlp1o=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17/go.mod h1:JgR/2Ew50ACfIWau1oeMRX59tMtC0kM+PYQGEaT04cY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 h1:W/EyPFl9A5rXrtoilfwHYEvzHER+K4SpBPtMXi24Mos=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 h1:W/EyPFl9A5rXrtoilfwHYEvzHER+K4SpBPtMXi24Mos=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18/go.mod h1:UG50K+pvd/uy6xExbobg0rjqFBFZe6I3l75EPDZw4tg=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18/go.mod h1:UG50K+pvd/uy6xExbobg0rjqFBFZe6I3l75EPDZw4tg=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 h1:dD3dhHNglpd98gs72my22Ndqi1hqQGllFFg1F+twfxg=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.38 h1:H/5TI1jqaHsNoDQ60UwvPvJBg4GURkinXI3Qga29t2w=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25/go.mod h1:0yAbjPfd64gG7mj85RW+fMEYdfBgCRZw8g/oWcL1pjc=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.38/go.mod h1:PTVFf+XH++7NJOky+RLBYQx0QA5NcaeEYFQ2fsi0nwo=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 h1:2pQEbwf+/6EDbiit/GcBE2K4IUpMZymaA0kOz3xK978=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 h1:2pQEbwf+/6EDbiit/GcBE2K4IUpMZymaA0kOz3xK978=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25/go.mod h1:KvT6NCcQ0EZ+ZkVRrlBMt04Po3ok23YELEp7WimhLhM=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25/go.mod h1:KvT6NCcQ0EZ+ZkVRrlBMt04Po3ok23YELEp7WimhLhM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 h1:ie4ElCmUKS26pzrZcIk/lmt4yWjAqLLcawstyQCh298=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 h1:ie4ElCmUKS26pzrZcIk/lmt4yWjAqLLcawstyQCh298=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2/go.mod h1:zjsomFeX5duj+4PlMB+o4JoWTIx+G0XMyzjYrUbQkN0=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2/go.mod h1:zjsomFeX5duj+4PlMB+o4JoWTIx+G0XMyzjYrUbQkN0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 h1:1VwbP3qMNfxUDEXWki4rCE5iA+44VA1lokTz9HasGzw=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.5.7 h1:YcczQ6zNH/ojIzD/ikDrO+RfW06wmdMp18d4NH5hXY4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.1/go.mod h1:vUtyoSj0OPji3kjIVSc/GlKuWEiL33f/WFxl6dmpy/A=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.5.7/go.mod h1:nl9RVnb9ulgAYzOkjLq1NyFxmWcnH2maCUEuOdESy98=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 h1:N6pIsdFOW1Kd9S4KyFKXdGRBojPPxkP32+uHFWLv4Hc=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.33.7 h1:P+bMNiA93gyuYT3Oh+4dWtvrnGcu2bd9Uy5hRJM8BNo=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.19/go.mod h1:3gt5WJArFooNmyLONS+h/R4J+o86II8du38IgCwj9dE=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.33.7/go.mod h1:zy+397isDFLvleg9H18Zq2MGzMso7uKyJyzR7DWSgFk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 h1:hc+lBYiiTr8Zk4MTzIsQ92MeDWCIDvWGmzKUWOaBcOg=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.7 h1:WWkehGZ4nWtOKLMy0yi8+RqzzVqAGe60hGaxwF06JAw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2/go.mod h1:hU6fqB3OJA6/ePheD47LQnxvjYk6br6PtQxs+Q9ojvk=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.7/go.mod h1:T8AI4SbQYm9ybcVmki2T3n7Qg1g3kfWoeQlNwNYOyO8=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3SJXQNEgksORW3Js=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.45.7 h1:yU/9y2r7s9kSUPbHXbpQTa4LA8kt+CMgpu1OBrhx8p4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.45.7/go.mod h1:0lQTDEBArMevQXpxu443LVGjKxxEeSsSnrw9n8YiTMg=
|
||||||
github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s=
|
github.com/aws/smithy-go v1.27.8 h1:FR0dxZfIlV7Z8eh2iHfIofdunw382XsDV3Mxt9nUvRY=
|
||||||
github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
github.com/aws/smithy-go v1.27.8/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
|
github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
|
||||||
|
|
|
||||||
|
|
@ -430,7 +430,7 @@ func authURLPort(value *url.URL) string {
|
||||||
|
|
||||||
// AuthConfig configures authentication for an upstream registry.
|
// AuthConfig configures authentication for an upstream registry.
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
// Type is the authentication type: "bearer", "basic", or "header".
|
// Type is the authentication type: "bearer", "basic", "header", or "ecr".
|
||||||
Type string `json:"type" yaml:"type"`
|
Type string `json:"type" yaml:"type"`
|
||||||
|
|
||||||
// Token is used for bearer authentication.
|
// Token is used for bearer authentication.
|
||||||
|
|
@ -450,6 +450,11 @@ type AuthConfig struct {
|
||||||
// HeaderValue is the custom header value (for type "header").
|
// HeaderValue is the custom header value (for type "header").
|
||||||
// Can reference environment variables with ${VAR_NAME} syntax.
|
// Can reference environment variables with ${VAR_NAME} syntax.
|
||||||
HeaderValue string `json:"header_value" yaml:"header_value"`
|
HeaderValue string `json:"header_value" yaml:"header_value"`
|
||||||
|
|
||||||
|
// Region is the AWS region for ECR authentication (for type "ecr").
|
||||||
|
// If empty, the AWS SDK default region chain is used (AWS_REGION,
|
||||||
|
// shared config, IMDS).
|
||||||
|
Region string `json:"region" yaml:"region"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default returns a Config with sensible defaults.
|
// Default returns a Config with sensible defaults.
|
||||||
|
|
|
||||||
126
internal/server/ecr_auth.go
Normal file
126
internal/server/ecr_auth.go
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ecrTokenTimeout = 10 * time.Second
|
||||||
|
ecrTokenSkew = 5 * time.Minute
|
||||||
|
ecrDefaultTokenLifetime = 12 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
var errEmptyECRToken = errors.New("empty ECR authorization token")
|
||||||
|
|
||||||
|
// ecrTokens caches AWS ECR authorization tokens per region and refreshes them
|
||||||
|
// on demand when they expire. Tokens are obtained via the AWS SDK default
|
||||||
|
// credential chain, so IAM roles for service accounts, instance profiles, and
|
||||||
|
// environment credentials all work without extra configuration.
|
||||||
|
type ecrTokens struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
cache map[string]ecrToken
|
||||||
|
sf singleflight.Group
|
||||||
|
|
||||||
|
// getToken fetches a fresh authorization token for the given region and
|
||||||
|
// returns the raw base64 "AWS:password" value plus its expiry. Overridable
|
||||||
|
// in tests.
|
||||||
|
getToken func(ctx context.Context, region string) (string, time.Time, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ecrToken struct {
|
||||||
|
value string
|
||||||
|
expiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func newECRTokens(logger *slog.Logger) *ecrTokens {
|
||||||
|
return &ecrTokens{
|
||||||
|
logger: logger,
|
||||||
|
cache: make(map[string]ecrToken),
|
||||||
|
getToken: fetchECRToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// header returns an Authorization header for the given region, fetching and
|
||||||
|
// caching a token on first use and after expiry. Concurrent misses for the
|
||||||
|
// same region share a single GetAuthorizationToken call. On failure it logs
|
||||||
|
// and returns empty strings so the request proceeds unauthenticated; the OCI
|
||||||
|
// transport then follows the Bearer challenge and surfaces the token-endpoint
|
||||||
|
// error, matching the behaviour of any other misconfigured upstream credential.
|
||||||
|
func (e *ecrTokens) header(region string) (name, value string) {
|
||||||
|
if tok, ok := e.cached(region); ok {
|
||||||
|
return "Authorization", tok.value
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err, _ := e.sf.Do(region, func() (any, error) {
|
||||||
|
if tok, ok := e.cached(region); ok {
|
||||||
|
return tok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), ecrTokenTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
raw, expiresAt, err := e.getToken(ctx, region)
|
||||||
|
if err != nil {
|
||||||
|
e.logger.Error("fetching ECR authorization token", "region", region, "error", err)
|
||||||
|
return ecrToken{}, err
|
||||||
|
}
|
||||||
|
if raw == "" {
|
||||||
|
e.logger.Error("ECR authorization token response was empty", "region", region)
|
||||||
|
return ecrToken{}, errEmptyECRToken
|
||||||
|
}
|
||||||
|
|
||||||
|
tok := ecrToken{value: "Basic " + raw, expiresAt: expiresAt.Add(-ecrTokenSkew)}
|
||||||
|
e.mu.Lock()
|
||||||
|
e.cache[region] = tok
|
||||||
|
e.mu.Unlock()
|
||||||
|
return tok, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Authorization", v.(ecrToken).value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ecrTokens) cached(region string) (ecrToken, bool) {
|
||||||
|
e.mu.Lock()
|
||||||
|
tok, ok := e.cache[region]
|
||||||
|
e.mu.Unlock()
|
||||||
|
return tok, ok && time.Now().Before(tok.expiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchECRToken(ctx context.Context, region string) (string, time.Time, error) {
|
||||||
|
var opts []func(*awsconfig.LoadOptions) error
|
||||||
|
if region != "" {
|
||||||
|
opts = append(opts, awsconfig.WithRegion(region))
|
||||||
|
}
|
||||||
|
cfg, err := awsconfig.LoadDefaultConfig(ctx, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := ecr.NewFromConfig(cfg).GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, err
|
||||||
|
}
|
||||||
|
if len(out.AuthorizationData) == 0 || out.AuthorizationData[0].AuthorizationToken == nil {
|
||||||
|
return "", time.Time{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data := out.AuthorizationData[0]
|
||||||
|
expiresAt := time.Now().Add(ecrDefaultTokenLifetime)
|
||||||
|
if data.ExpiresAt != nil {
|
||||||
|
expiresAt = *data.ExpiresAt
|
||||||
|
}
|
||||||
|
return *data.AuthorizationToken, expiresAt, nil
|
||||||
|
}
|
||||||
159
internal/server/ecr_auth_test.go
Normal file
159
internal/server/ecr_auth_test.go
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/git-pkgs/proxy/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testECRTokens() *ecrTokens {
|
||||||
|
return newECRTokens(slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECRTokensCachesUntilExpiry(t *testing.T) {
|
||||||
|
e := testECRTokens()
|
||||||
|
calls := 0
|
||||||
|
e.getToken = func(_ context.Context, region string) (string, time.Time, error) {
|
||||||
|
calls++
|
||||||
|
if region != "eu-west-1" {
|
||||||
|
t.Errorf("region = %q, want eu-west-1", region)
|
||||||
|
}
|
||||||
|
return "QVdTOnNlY3JldA==", time.Now().Add(12 * time.Hour), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
name, value := e.header("eu-west-1")
|
||||||
|
if name != "Authorization" || value != "Basic QVdTOnNlY3JldA==" {
|
||||||
|
t.Fatalf("header() = %q, %q", name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.header("eu-west-1")
|
||||||
|
e.header("eu-west-1")
|
||||||
|
if calls != 1 {
|
||||||
|
t.Fatalf("getToken called %d times, want 1", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECRTokensRefreshesAfterExpiry(t *testing.T) {
|
||||||
|
e := testECRTokens()
|
||||||
|
calls := 0
|
||||||
|
e.getToken = func(_ context.Context, _ string) (string, time.Time, error) {
|
||||||
|
calls++
|
||||||
|
return "dG9rZW4=", time.Now().Add(time.Minute), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
e.header("us-east-1")
|
||||||
|
e.header("us-east-1")
|
||||||
|
if calls != 2 {
|
||||||
|
t.Fatalf("getToken called %d times, want 2 (token within skew window)", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECRTokensPerRegion(t *testing.T) {
|
||||||
|
e := testECRTokens()
|
||||||
|
seen := map[string]int{}
|
||||||
|
e.getToken = func(_ context.Context, region string) (string, time.Time, error) {
|
||||||
|
seen[region]++
|
||||||
|
return region + "-token", time.Now().Add(time.Hour), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
e.header("eu-west-1")
|
||||||
|
e.header("us-east-1")
|
||||||
|
e.header("eu-west-1")
|
||||||
|
|
||||||
|
if seen["eu-west-1"] != 1 || seen["us-east-1"] != 1 {
|
||||||
|
t.Fatalf("per-region calls = %v, want one each", seen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECRTokensConcurrentMissesShareOneFetch(t *testing.T) {
|
||||||
|
e := testECRTokens()
|
||||||
|
var calls atomic.Int32
|
||||||
|
release := make(chan struct{})
|
||||||
|
e.getToken = func(_ context.Context, _ string) (string, time.Time, error) {
|
||||||
|
calls.Add(1)
|
||||||
|
<-release
|
||||||
|
return "dG9rZW4=", time.Now().Add(time.Hour), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = 10
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(n)
|
||||||
|
for range n {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
name, value := e.header("eu-west-1")
|
||||||
|
if name != "Authorization" || value != "Basic dG9rZW4=" {
|
||||||
|
t.Errorf("header() = %q, %q", name, value)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
close(release)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if got := calls.Load(); got != 1 {
|
||||||
|
t.Fatalf("getToken called %d times, want 1", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECRTokensErrorReturnsNoAuth(t *testing.T) {
|
||||||
|
e := testECRTokens()
|
||||||
|
e.getToken = func(_ context.Context, _ string) (string, time.Time, error) {
|
||||||
|
return "", time.Time{}, errors.New("no credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
name, value := e.header("eu-west-1")
|
||||||
|
if name != "" || value != "" {
|
||||||
|
t.Fatalf("header() = %q, %q; want empty on error", name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthForURLRoutesECRType(t *testing.T) {
|
||||||
|
e := testECRTokens()
|
||||||
|
e.getToken = func(_ context.Context, region string) (string, time.Time, error) {
|
||||||
|
if region != "eu-west-1" {
|
||||||
|
t.Errorf("region = %q, want eu-west-1", region)
|
||||||
|
}
|
||||||
|
return "QVdTOnNlY3JldA==", time.Now().Add(time.Hour), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Server{
|
||||||
|
ecr: e,
|
||||||
|
cfg: &config.Config{
|
||||||
|
Upstream: config.UpstreamConfig{
|
||||||
|
Auth: map[string]config.AuthConfig{
|
||||||
|
"https://123456789012.dkr.ecr.eu-west-1.amazonaws.com": {
|
||||||
|
Type: "ecr",
|
||||||
|
Region: "eu-west-1",
|
||||||
|
},
|
||||||
|
"https://ghcr.io": {
|
||||||
|
Type: "bearer",
|
||||||
|
Token: "ghcr-token",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
name, value := s.authForURL("https://123456789012.dkr.ecr.eu-west-1.amazonaws.com/v2/my/repo/manifests/latest")
|
||||||
|
if name != "Authorization" || value != "Basic QVdTOnNlY3JldA==" {
|
||||||
|
t.Fatalf("ecr authForURL() = %q, %q", name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
name, value = s.authForURL("https://ghcr.io/v2/owner/repo/blobs/sha256:abc")
|
||||||
|
if name != "Authorization" || value != "Bearer ghcr-token" {
|
||||||
|
t.Fatalf("bearer authForURL() = %q, %q", name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
name, value = s.authForURL("https://registry-1.docker.io/v2/")
|
||||||
|
if name != "" || value != "" {
|
||||||
|
t.Fatalf("unmatched authForURL() = %q, %q; want empty", name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -98,6 +98,7 @@ type Server struct {
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
healthCache *healthCache
|
healthCache *healthCache
|
||||||
accessLog *accesslog.Logger
|
accessLog *accesslog.Logger
|
||||||
|
ecr *ecrTokens
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Server with the given configuration.
|
// New creates a new Server with the given configuration.
|
||||||
|
|
@ -174,6 +175,7 @@ func New(cfg *config.Config, logger *slog.Logger, buildInfo BuildInfo) (*Server,
|
||||||
templates: &Templates{},
|
templates: &Templates{},
|
||||||
healthCache: hc,
|
healthCache: hc,
|
||||||
accessLog: activityLog,
|
accessLog: activityLog,
|
||||||
|
ecr: newECRTokens(logger),
|
||||||
}
|
}
|
||||||
closeAccessLog = false
|
closeAccessLog = false
|
||||||
return server, nil
|
return server, nil
|
||||||
|
|
@ -425,6 +427,9 @@ func (s *Server) authForURL(url string) (headerName, headerValue string) {
|
||||||
if auth == nil {
|
if auth == nil {
|
||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
|
if strings.EqualFold(auth.Type, "ecr") {
|
||||||
|
return s.ecr.header(auth.Region)
|
||||||
|
}
|
||||||
return auth.Header()
|
return auth.Header()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue