Skip to content

Commit ddbd98e

Browse files
committed
Keep Helm repository credentials on their configured origin
1 parent 73f2994 commit ddbd98e

7 files changed

Lines changed: 666 additions & 52 deletions

File tree

‎docs/helm.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,14 @@ Programmatic upgrades require a confirmed saved source or an explicit source sel
153153

154154
Explicit source changes use `POST /api/helm/releases/{namespace}/{name}/source/preview`, followed by `/source/upgrade` or `/source/upgrade-stream`. These routes require the `sourceUpgrade` selection, explicit values and an exact version (body `version` for preview, query `version` for upgrade). Preview returns `previewDigest`; send it back in the selection when applying. The selection also carries `expectedRevision` and `expectedSource` from source status. Older backends return 404 for these routes instead of ignoring the selected source. Ordinary upgrade routes reject source-change fields.
155155

156+
### Repository credentials and redirects
157+
158+
For HTTP repositories, Radar sends repository passwords and client certificates only to the configured address's scheme, hostname and port. A redirect or a chart URL on another origin is fetched without those credentials by default. Public chart hosts are fetched without repository credentials. By default, on a different origin, Radar verifies the server certificate using system trust plus any configured CA, even if verification was skipped for the repository.
159+
160+
If a private repository deliberately shares credentials with its chart host, its Helm configuration must explicitly enable `pass_credentials_all`. This also permits sending the client certificate to that host. Use it only for hosts you trust. Redirected Basic authentication still follows Go's hostname restrictions; Radar does not restore a header Go removed.
161+
162+
Custom CA certificates, client certificates and `insecure_skip_tls_verify` remain supported, including configurations with both a CA file and skipped certificate verification. If a repository address redirects from HTTP to HTTPS, configure its final HTTPS address to keep authenticated requests on the configured origin.
163+
156164
### Chart download checksums
157165

158166
When an HTTP repository index advertises a SHA-256 checksum, Radar checks the downloaded chart archive before reading or using it. An invalid checksum or a mismatch stops chart details, installation, source recording and any upgrade or values operation that downloads that chart. The release stays unchanged.

‎internal/helm/chart_sources.go‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"strings"
1717

1818
"helm.sh/helm/v3/pkg/action"
19-
"helm.sh/helm/v3/pkg/getter"
2019
"helm.sh/helm/v3/pkg/release"
2120
"helm.sh/helm/v3/pkg/repo"
2221
orasregistry "oras.land/oras-go/v2/registry"
@@ -347,7 +346,7 @@ func (c *Client) ensureClassicRepository(rawURL, preferredName string) (string,
347346
return "", fmt.Errorf("stage repository index: %w", err)
348347
}
349348
defer os.RemoveAll(stageDir)
350-
chartRepo, err := repo.NewChartRepository(entry, getter.All(c.settings))
349+
chartRepo, err := repo.NewChartRepository(entry, c.repositoryGetters(entry))
351350
if err != nil {
352351
return "", fmt.Errorf("failed to create chart repository: %w", err)
353352
}

‎internal/helm/client.go‎

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import (
3535
"helm.sh/helm/v3/pkg/chart/loader"
3636
"helm.sh/helm/v3/pkg/chartutil"
3737
"helm.sh/helm/v3/pkg/cli"
38-
"helm.sh/helm/v3/pkg/getter"
3938
"helm.sh/helm/v3/pkg/kube"
4039
"helm.sh/helm/v3/pkg/registry"
4140
"helm.sh/helm/v3/pkg/release"
@@ -60,6 +59,16 @@ var repositoryRefreshErrorURL = regexp.MustCompile(`[a-zA-Z][a-zA-Z0-9+.-]*://[^
6059

6160
// HTTP client for ArtifactHub requests
6261
var httpClient = &http.Client{
62+
CheckRedirect: func(req *http.Request, via []*http.Request) error {
63+
if len(via) >= 10 {
64+
return fmt.Errorf("stopped after 10 redirects")
65+
}
66+
if !sameHTTPOrigin(via[0].URL, req.URL) {
67+
req.Header.Del("Authorization")
68+
req.Header.Del("Cookie")
69+
}
70+
return nil
71+
},
6372
Timeout: 30 * time.Second,
6473
}
6574

@@ -3176,7 +3185,7 @@ func (c *Client) UpdateRepository(repoName string) error {
31763185
}
31773186

31783187
// Create chart repository and download index
3179-
chartRepo, err := repo.NewChartRepository(repoEntry, getter.All(c.settings))
3188+
chartRepo, err := repo.NewChartRepository(repoEntry, c.repositoryGetters(repoEntry))
31803189
if err != nil {
31813190
return fmt.Errorf("failed to create chart repository %q: %s", repoName, repositoryRefreshErrorURL.ReplaceAllString(err.Error(), "<redacted>"))
31823191
}
@@ -3365,25 +3374,32 @@ func (c *Client) GetChartDetail(repoName, chartName, version string) (*ChartDeta
33653374
chartURL = strings.TrimSuffix(repoEntry.URL, "/") + "/" + chartURL
33663375
}
33673376

3368-
cp, err := c.locateChartPath(&action.Configuration{}, chartURL, chartVersion.Version)
3369-
if err != nil {
3370-
// If we can't download, return basic info from index
3371-
return &ChartDetail{
3372-
ChartInfo: chartVersionToInfo(chartVersion, repoName),
3373-
}, nil
3377+
var loadedChart *chart.Chart
3378+
if registry.IsOCI(chartURL) {
3379+
cp, locateErr := c.locateChartPath(nil, chartURL, chartVersion.Version)
3380+
if locateErr != nil {
3381+
return &ChartDetail{ChartInfo: chartVersionToInfo(chartVersion, repoName)}, nil
3382+
}
3383+
loadedChart, err = loadChartWithDigest(cp, chartVersion.Digest)
3384+
} else {
3385+
body, fetchErr := c.getRepositoryChart(repoEntry, chartURL)
3386+
if fetchErr != nil {
3387+
log.Printf("[helm] Could not download chart details for %s/%s; showing repository index information: %v", repoName, chartName, fetchErr)
3388+
return &ChartDetail{ChartInfo: chartVersionToInfo(chartVersion, repoName)}, nil
3389+
}
3390+
if err := verifyChartDigest(body.Bytes(), chartVersion.Digest); err != nil {
3391+
return nil, fmt.Errorf("chart %s version %s: %w", chartName, chartVersion.Version, withChartDigestGuidance(err, false))
3392+
}
3393+
loadedChart, err = loader.LoadArchive(body)
33743394
}
3375-
3376-
chart, err := loadChartWithDigest(cp, chartVersion.Digest)
33773395
if err != nil {
33783396
if errors.Is(err, errChartDigest) {
33793397
return nil, fmt.Errorf("chart %s version %s: %w", chartName, chartVersion.Version, withChartDigestGuidance(err, false))
33803398
}
3381-
return &ChartDetail{
3382-
ChartInfo: chartVersionToInfo(chartVersion, repoName),
3383-
}, nil
3399+
return &ChartDetail{ChartInfo: chartVersionToInfo(chartVersion, repoName)}, nil
33843400
}
33853401

3386-
return chartDetailFromChart(chart, chartVersionToInfo(chartVersion, repoName)), nil
3402+
return chartDetailFromChart(loadedChart, chartVersionToInfo(chartVersion, repoName)), nil
33873403
}
33883404

33893405
func chartDetailFromChart(chart *chart.Chart, info ChartInfo) *ChartDetail {
@@ -3448,6 +3464,7 @@ func (c *Client) installWith(actionConfig *action.Configuration, req *InstallReq
34483464

34493465
var chartURL string
34503466
var indexDigest string
3467+
var downloadEntry *repo.Entry
34513468
resolvedVersion := req.Version
34523469

34533470
// Check if the repository is a URL (for ArtifactHub installs) or a local repo name
@@ -3461,6 +3478,7 @@ func (c *Client) installWith(actionConfig *action.Configuration, req *InstallReq
34613478
return nil, err
34623479
}
34633480
} else if isRepoURL {
3481+
downloadEntry = &repo.Entry{URL: req.Repository}
34643482
// Direct URL - fetch the repository index to find the chart
34653483
repoURL := strings.TrimSuffix(req.Repository, "/")
34663484

@@ -3542,6 +3560,7 @@ func (c *Client) installWith(actionConfig *action.Configuration, req *InstallReq
35423560
}
35433561
}
35443562

3563+
downloadEntry = repoEntry
35453564
if repoEntry == nil {
35463565
return nil, fmt.Errorf("repository %s not found", req.Repository)
35473566
}
@@ -3599,19 +3618,31 @@ func (c *Client) installWith(actionConfig *action.Configuration, req *InstallReq
35993618
}
36003619
req.resolvedSource = resolvedSource
36013620

3602-
cp, err := c.locateChartPath(actionConfig, chartURL, resolvedVersion)
3603-
if err != nil {
3604-
return nil, fmt.Errorf("failed to locate chart: %w", err)
3621+
var loadedChart *chart.Chart
3622+
if registry.IsOCI(chartURL) {
3623+
cp, locateErr := c.locateChartPath(actionConfig, chartURL, resolvedVersion)
3624+
if locateErr != nil {
3625+
return nil, fmt.Errorf("failed to locate chart: %w", locateErr)
3626+
}
3627+
loadedChart, err = loadChartWithDigest(cp, indexDigest)
3628+
} else {
3629+
body, fetchErr := c.getRepositoryChart(downloadEntry, chartURL)
3630+
if fetchErr != nil {
3631+
return nil, fmt.Errorf("failed to download chart: %w", fetchErr)
3632+
}
3633+
if err := verifyChartDigest(body.Bytes(), indexDigest); err != nil {
3634+
return nil, fmt.Errorf("chart %s version %s: %w", req.ChartName, resolvedVersion, withChartDigestGuidance(err, isRepoURL))
3635+
}
3636+
loadedChart, err = loader.LoadArchive(body)
36053637
}
3606-
chart, err := loadChartWithDigest(cp, indexDigest)
36073638
if err != nil {
36083639
return nil, fmt.Errorf("failed to load chart %s version %s: %w", req.ChartName, resolvedVersion, withChartDigestGuidance(err, isRepoURL))
36093640
}
36103641

36113642
if mode != installFresh {
36123643
log.Printf("[helm] install %q/%q: prior release record exists, recovering via %s", req.Namespace, req.ReleaseName, recoveryMode(mode))
36133644
}
3614-
rel, err := runInstallOrUpgrade(actionConfig, req, chart, mode)
3645+
rel, err := runInstallOrUpgrade(actionConfig, req, loadedChart, mode)
36153646
if err != nil {
36163647
return nil, fmt.Errorf("install failed: %w", err)
36173648
}
@@ -3648,6 +3679,7 @@ func (c *Client) installWithProgressUsing(actionConfig *action.Configuration, re
36483679

36493680
var chartURL string
36503681
var indexDigest string
3682+
var downloadEntry *repo.Entry
36513683
resolvedVersion := req.Version
36523684

36533685
// Check if the repository is a URL (for ArtifactHub installs) or a local repo name
@@ -3662,6 +3694,7 @@ func (c *Client) installWithProgressUsing(actionConfig *action.Configuration, re
36623694
}
36633695
sendProgress("resolving", "Resolving chart from OCI registry...", chartURL)
36643696
} else if isRepoURL {
3697+
downloadEntry = &repo.Entry{URL: req.Repository}
36653698
sendProgress("fetching", "Fetching repository index...", req.Repository)
36663699

36673700
repoURL := strings.TrimSuffix(req.Repository, "/")
@@ -3741,6 +3774,7 @@ func (c *Client) installWithProgressUsing(actionConfig *action.Configuration, re
37413774
}
37423775
}
37433776

3777+
downloadEntry = repoEntry
37443778
if repoEntry == nil {
37453779
return nil, fmt.Errorf("repository %s not found", req.Repository)
37463780
}
@@ -3813,35 +3847,19 @@ func (c *Client) installWithProgressUsing(actionConfig *action.Configuration, re
38133847
return nil, fmt.Errorf("failed to load chart %s version %s: %w", req.ChartName, resolvedVersion, withChartDigestGuidance(err, isRepoURL))
38143848
}
38153849
} else {
3816-
// Preserve the existing direct HTTP path. Helm's ChartDownloader loads all
3817-
// locally registered indexes and can fail because an unrelated cache entry
3818-
// is stale or missing.
3819-
chartResp, err := httpClient.Get(chartURL)
3820-
if err != nil {
3821-
return nil, fmt.Errorf("failed to download chart: %w", err)
3850+
body, fetchErr := c.getRepositoryChart(downloadEntry, chartURL)
3851+
if fetchErr != nil {
3852+
return nil, fmt.Errorf("failed to download chart: %w", fetchErr)
38223853
}
3823-
defer chartResp.Body.Close()
3824-
if chartResp.StatusCode != 200 {
3825-
return nil, fmt.Errorf("failed to download chart: server returned %d", chartResp.StatusCode)
3854+
sendProgress("loading", "Loading chart...", "")
3855+
if err := verifyChartDigest(body.Bytes(), indexDigest); err != nil {
3856+
return nil, fmt.Errorf("chart %s version %s: %w", req.ChartName, resolvedVersion, withChartDigestGuidance(err, isRepoURL))
38263857
}
3827-
3828-
tmpChart, err := os.CreateTemp("", "helm-chart-*.tgz")
3829-
if err != nil {
3830-
return nil, fmt.Errorf("failed to create temp file for chart: %w", err)
3831-
}
3832-
defer os.Remove(tmpChart.Name())
3833-
defer tmpChart.Close()
3834-
3835-
if _, err := tmpChart.ReadFrom(chartResp.Body); err != nil {
3836-
return nil, fmt.Errorf("failed to write chart to temp file: %w", err)
3837-
}
3838-
tmpChart.Close()
3839-
3840-
sendProgress("loading", "Loading chart...", tmpChart.Name())
3841-
loadedChart, err = loadChartWithDigest(tmpChart.Name(), indexDigest)
3858+
loadedChart, err = loader.LoadArchive(body)
38423859
if err != nil {
38433860
return nil, fmt.Errorf("failed to load chart %s version %s: %w", req.ChartName, resolvedVersion, withChartDigestGuidance(err, isRepoURL))
38443861
}
3862+
38453863
}
38463864

38473865
switch mode {

‎internal/helm/prepared_install.go‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,18 @@ func (c *Client) resolvePreparedChart(ctx context.Context, req *InstallRequest,
319319
if err != nil {
320320
return nil, "", err
321321
}
322-
body, err := fetchPreparedBytes(ctx, chartURL, maxPreparedChartBytes, "chart")
322+
entry, err := c.repositoryEntryForRequest(req.Repository)
323323
if err != nil {
324324
return nil, "", err
325325
}
326+
buffer, err := (repositoryHTTPGetter{entry: *entry, timeout: httpClient.Timeout, maxBytes: maxPreparedChartBytes}).getContext(ctx, chartURL)
327+
var body []byte
328+
if buffer != nil {
329+
body = buffer.Bytes()
330+
}
331+
if err != nil {
332+
return nil, "", fmt.Errorf("fetch chart: %w", err)
333+
}
326334
actual := sha256.Sum256(body)
327335
if !bytes.Equal(actual[:], want) {
328336
return nil, "", fmt.Errorf("chart %s version %s digest does not match repository index", req.ChartName, resolved)

0 commit comments

Comments
 (0)