Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-08-23 04:14:57 -04:00

Compare commits

..
8 changed files with 43 additions and 365 deletions

View file

@ -120,7 +120,6 @@ 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":
@ -144,13 +143,6 @@ 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:

View file

@ -216,22 +216,6 @@ 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
View file

@ -7,8 +7,6 @@ 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
@ -76,24 +74,25 @@ 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.43.7 // indirect github.com/aws/aws-sdk-go-v2 v1.41.9 // 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/credentials v1.19.37 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.20 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38 // 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.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.38 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.38 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.39 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 // 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/accept-encoding v1.13.10 // 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.38 // 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/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.5.7 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.33.7 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.7 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.45.7 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 // indirect
github.com/aws/smithy-go v1.27.8 // indirect github.com/aws/smithy-go v1.26.0 // 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
View file

@ -117,46 +117,44 @@ 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.43.7 h1:msCzvkeYJA9ehbV8mRRmkZLo/zJg/+yDVLNtflg83hQ= github.com/aws/aws-sdk-go-v2 v1.41.9 h1:/rYeyO2+HrMztAmxAq9++XJtFMqSIpSsNA0yDGALYq4=
github.com/aws/aws-sdk-go-v2 v1.43.7/go.mod h1:tXpPM+v0D1lndmga+HqqLDIzUFJlEeR21aspVklHF00= github.com/aws/aws-sdk-go-v2 v1.41.9/go.mod h1:+HsoOEX80qAVUitj1A2DhCNTjmb3edVyuDypb6LNEeo=
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.38 h1:n4yPHBjtQ3BrIIUyk0/LAqf/BL2iv0Tw6XZcMRzM0ps= 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/go.mod h1:dencYsOS1R7rBy8zehCvwBYzdxxL4Q/nRK7In03wjN8= github.com/aws/aws-sdk-go-v2/config v1.32.20/go.mod h1:PuwEpciweIXGULWeOeSTXtSbH4CW9mWdWrhdCKQI1sM=
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 h1:yuFzSV1U0aRNYCQGVaTY2zW2M/L93pYHnXnrJUphYhU=
github.com/aws/aws-sdk-go-v2/credentials v1.19.37/go.mod h1:Q6pWOgVUp49x4g5QVi29wHofUoICnZ+Zq4jHbRN/7ec= github.com/aws/aws-sdk-go-v2/credentials v1.19.19/go.mod h1:7y63L1kGzeoDlJaQ3Z578KrnmfBut96JjvJUzGwR+YE=
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 h1:0w6dCiO8iez+YKwRhRBlL1CH/E3GTfdkuzrwj1by8vo=
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/ec2/imds v1.18.25/go.mod h1:9FDWUothyr5RCRAHc45XOiVCzUR8n/IhCYX+uVqw6vk=
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.38 h1:MBMg0zJ6i4TkAJ0dVFLKKn2cOkY6FkicmUDM67BRr6g= 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/go.mod h1:9MWuJbyiUyj6eA7W1/zm1zuePDPSB3g+xcgRQeMWsXc= 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/endpoints/v2 v2.7.38 h1:lHm4jPf3k1Lz5ZWc+Vcn3MKVwym+26kWCba9FkJ4f0Y= 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/go.mod h1:Rn+P2XR+FbyZzjmWKjg/KUZNxmGfr5oZwh5jQiE+CzI= 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/v4a v1.4.39 h1:vo4xvMRs/F6h1E52qsgLqCQgWIQXgIJUauG6rlZEh4U= 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/go.mod h1:jB03R1ij/A+OE2e1dz6vgj076gd7vlYcfstAzj3HcnU= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26/go.mod h1:dY4MRzXEizrD4hqtpKvWVGPX7QleSGGVY+EBolo1RmM=
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 h1:d5/908OJ4bXg8lyjeMPvXetEKqoDoLi5Owy1zNue3yg=
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.10/go.mod h1:a57l7Hwh+FWI+we50g5NPJHYUKeJKfXbc4w8SyXu8Ig=
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.38 h1:H/5TI1jqaHsNoDQ60UwvPvJBg4GURkinXI3Qga29t2w= 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/go.mod h1:PTVFf+XH++7NJOky+RLBYQx0QA5NcaeEYFQ2fsi0nwo= 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/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.5.7 h1:YcczQ6zNH/ojIzD/ikDrO+RfW06wmdMp18d4NH5hXY4= 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/go.mod h1:nl9RVnb9ulgAYzOkjLq1NyFxmWcnH2maCUEuOdESy98= 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/sso v1.33.7 h1:P+bMNiA93gyuYT3Oh+4dWtvrnGcu2bd9Uy5hRJM8BNo= 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/go.mod h1:zy+397isDFLvleg9H18Zq2MGzMso7uKyJyzR7DWSgFk= 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/ssooidc v1.38.7 h1:WWkehGZ4nWtOKLMy0yi8+RqzzVqAGe60hGaxwF06JAw= 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/go.mod h1:T8AI4SbQYm9ybcVmki2T3n7Qg1g3kfWoeQlNwNYOyO8= 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/sts v1.45.7 h1:yU/9y2r7s9kSUPbHXbpQTa4LA8kt+CMgpu1OBrhx8p4= 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/go.mod h1:0lQTDEBArMevQXpxu443LVGjKxxEeSsSnrw9n8YiTMg= github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8=
github.com/aws/smithy-go v1.27.8 h1:FR0dxZfIlV7Z8eh2iHfIofdunw382XsDV3Mxt9nUvRY= github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s=
github.com/aws/smithy-go v1.27.8/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aws/smithy-go v1.26.0/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=

View file

@ -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", "header", or "ecr". // Type is the authentication type: "bearer", "basic", or "header".
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,11 +450,6 @@ 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.

View file

@ -1,126 +0,0 @@
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
}

View file

@ -1,159 +0,0 @@
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)
}
}

View file

@ -98,7 +98,6 @@ 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.
@ -175,7 +174,6 @@ 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
@ -427,9 +425,6 @@ 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()
} }