rfc/oci-registries: Assorted changes to finalize the next draft

This includes both the "Module implementation details" appendix and a
general copyediting pass just to try to make the writing style a little
more consistent, since this content was originally written by two
different authors with different style preferences.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins 2025-02-20 19:28:36 -08:00
parent f9e7b10a53
commit 0eb59c1d0f
11 changed files with 238 additions and 136 deletions

View File

@ -8,14 +8,14 @@ OCI registries (also historically known as Docker registries) form the backbone
However, thanks to the generic architecture of OCI registries, several implementations have popped up that allow users to store arbitrary data in OCI registries beyond container images. For example, [OCI Registry As Storage (ORAS)](https://oras.land/) is such a project.
The OCI registry standardization and the design of OpenTofu's [Provider Registry Protocol](https://opentofu.org/docs/internals/provider-registry-protocol/) and [Module Registry Protocol](https://opentofu.org/docs/internals/module-registry-protocol/) happened around the same time. These protocols followed different design goals. For example, OpenTofu's provider registry protocol concerns itself with artifact signing and decoupling the index and the download part of the registry, whereas OCI is concerned, for example, with layer pull efficiency. At the time of writing this RFC OCI artifact signing is still in development and only a partially solved problem, with projects like [sigstore and cosign](https://www.sigstore.dev/) aiming to address the issue.
The OCI registry standardization and the design of OpenTofu's [Provider Registry Protocol](https://opentofu.org/docs/internals/provider-registry-protocol/) and [Module Registry Protocol](https://opentofu.org/docs/internals/module-registry-protocol/) happened around the same time. These protocols followed different design goals. For example, OpenTofu's provider registry protocol concerns itself with artifact signing and decoupling the index and the download part of the registry, whereas OCI Distribution has other priorities such as layer pull efficiency. At the time of writing this RFC OCI artifact signing is still in development and only a partially solved problem, with projects like [sigstore and cosign](https://www.sigstore.dev/) aiming to address the issue.
> [!NOTE]
> We have created a [primer on OCI](20241206-oci-registries/1-oci-primer.md) for this RFC. If you are unfamiliar with the protocol, you may want to read it before reading this RFC.
## Why OCI?
Many users of OpenTofu work in large organizations, somtimes with air-gapped environments. Given the popularity of [Kubernetes](https://kubernetes.io/) in larger organizations, OCI registries are widely available without any additional compliance burden. Cloud providers also offer a range of options, and public container registries such as DockerHub or GitHub Container Registry are also available.
Many users of OpenTofu work in large organizations, sometimes with air-gapped environments. Given the popularity of [Kubernetes](https://kubernetes.io/) in larger organizations, OCI registries are widely available without any additional compliance burden. Cloud providers also offer a range of options, and public container registries such as DockerHub or GitHub Container Registry are also available.
In contrast, running an OpenTofu / Terraform registry requires the setup of [an extra piece of software](https://awesome-opentofu.com/#registry), which incurs additional costs when run publicly and an additional compliance burden when run in an organization.
@ -43,7 +43,7 @@ The following additional sections discuss potential implementation details of th
## Potential alternatives
OCI resolves a very real pain-point for enterprise users wanting to run a private registry. A potential alternative would be, of course, making the use of a private registry easier, or creating a tool that can maintain a private registry purely based on static files. On that note, we could also implement [running the OpenTofu Registry on the same dataset privately](https://github.com/opentofu/registry/issues/1518).
OCI Registry support resolves a very real pain-point for enterprise users wanting to run a private registry. A potential alternative would be making the use of a private registry easier, or creating a tool that can maintain a private registry purely based on static files. On that note, we could also implement [running the OpenTofu Registry on the same dataset privately](https://github.com/opentofu/registry/issues/1518).
These solutions would also work towards the goal of making the ecosystem fully decentralized.
@ -51,7 +51,7 @@ That being said, neither of these solutions are as convenient as OCI since the i
## Future plans
When it comes to providers, the current RFC mainly addresses OCI as a private mirror of providers whose origin is an OpenTofu provider registry. This is a limitation stemming from the fact that provider addresses are virtual, as described in [Design Considerations](20241206-oci-registries/3-design-considerations.md). In a future OpenTofu version we would like to address this shortcoming and enable everyone to use OCI for a new kind of provider registry without relying on configuration options in the `.tofurc` file.
When it comes to providers, the current RFC mainly addresses OCI as an operator-configured mirror of providers whose origin is an OpenTofu provider registry. This is a limitation stemming from the fact that provider addresses are virtual, as described in [Design Considerations](20241206-oci-registries/3-design-considerations.md). In a future OpenTofu version we would like to address this shortcoming and enable everyone to use OCI for a new kind of provider registry without relying on configuration options in the `.tofurc` file.
Additionally, currently the OCI registry implementation doesn't have the equivalent of the [OpenTofu Registry Search](https://search.opentofu.org). We envision this to be resolved similar to how Linux packages often publish a supplemental `doc` package, containing the related documentation. This, however, will need tooling in OpenTofu to render and show that documentation. While much of the code can be reused from the [Search source code](https://github.com/opentofu/registry-ui), that project was not written with reuse in mind and will need some additional work to adapt as a general-purpose documentation viewer.

View File

@ -8,35 +8,35 @@ This document is part of the [OCI registries RFC](../20241206-oci-registries.md)
---
OCI registries provide an HTTP interface to access *manifests* and *blobs*. Manifests describe the content in the registry, while blobs are binary data. The specification for the protocol is outlined in the [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec/blob/main/spec.md) The content stored in the OCI registry must follow the [OCI Image Format Specification](https://github.com/opencontainers/image-spec/blob/v1.0.1/spec.md).
OCI registries provide an HTTP interface to access *manifests* and *blobs*. Manifests are metadata describing the content in the registry, while blobs are the actual data. The specification for the protocol is outlined in the [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec/blob/main/spec.md). The content stored in the OCI registry must follow the [OCI Image Format Specification](https://github.com/opencontainers/image-spec/blob/v1.0.1/spec.md).
It's worth noting that many registry implementations, regarding ghcr.io, don't follow the OCI image format and instead return [Docker Image Manifest](https://distribution.github.io/distribution/spec/manifest-v2-2/) documents. However, the differences are very minor and this document will focus on the OCI specifications. During implementation these differences must be addressed.
It's worth noting that many registry implementations don't yet follow the OCI image format and instead return [Docker Image Manifest](https://distribution.github.io/distribution/spec/manifest-v2-2/) documents. However, the differences are very minor and this document will focus on the OCI specifications. During implementation these differences must be addressed.
> [!TIP]
> There is some flexibility in how the data is stored, which also gave rise to [ORAS](https://oras.land/) (OCI Registry as Storage). For details, see the [ORAS section below](#oras).
> [!WARNING]
> The examples in this document are meant to showcase the protocol only, they are not indicative of how OpenTofu stores data in OCI!
> The examples in this document are meant to illustrate the plain OCI Distribution protocol only. They are not part of the proposal for how we intend to capture OpenTofu-specific artifacts (providers or modules) in OCI registries.
## Authentication
Although registries don't necessarily need authentication, many public registries, such as `ghcr.io` and the Docker Hub require an anonymous token even to access public images. When accessing an endpoint, a client may receive a `WWW-Authenticate` header, indicating that authentication must be performed.
Although registries don't necessarily need authentication, many public registries (including `ghcr.io` and Docker Hub) require an "anonymous" token even to access public images. When accessing an endpoint, the registry server might send a `WWW-Authenticate` header field, indicating that authentication is needed.
For example, accessing `https://ghcr.io/v2/opentofu/opentofu/tags/list` will return the following header:
For example, accessing `https://ghcr.io/v2/opentofu/opentofu/tags/list` will return the following header field:
```
www-authenticate: Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:opentofu/opentofu:pull"
```
It is worth noting that accessing the base endpoint of `/v2/` will not yield a valid scope on `ghcr.io` and should not be used for authentication. The `realm` field in the `WWW-Authenticate` indicates the endpoint to use for authentication. We can perform the authentication by performing a simple get request:
It is worth noting that accessing the base endpoint of `/v2/` will not yield a valid scope on `ghcr.io` and should not be used for authentication. The `realm` field in the `WWW-Authenticate` indicates the endpoint to use for authentication, and so we can perform the authentication by performing a `GET` request to exchange (optional) credentials for a temporary bearer token:
```
```shell
curl -u user:password 'https://ghcr.io/token?service=ghcr.io&scope=repository:opentofu/opentofu:pull'
```
The username/password part is optional for public registries and the response will contain a token we can use for authentication:
If successful, the registry server's response will contain a temporary bearer token we can use for subsequent requests:
```
```json
{"token":"djE6b3Blb..."}
```
@ -46,9 +46,9 @@ The username/password part is optional for public registries and the response wi
> curl -v 'https://ghcr.io/token?service=ghcr.io&scope=repository:opentofu/opentofu:pull'
> ```
### Index vs. image manifest
### Index vs. image manifests
Manifests can have two types. An index (media type of `application/vnd.oci.image.index.v1+json` or `application/vnd.docker.distribution.manifest.list.v2+json`) contains a list of image manifests. This is useful when you want to distribute your image for multiple platforms.
Manifests can be of two different types. An index manifest (media type `application/vnd.oci.image.index.v1+json` or `application/vnd.docker.distribution.manifest.list.v2+json`) contains a list of image manifests and platform-selection information associated with each one. This is needed when distributing separate artifacts for each target platform.
> [!TIP]
> Try it yourself: authenticate with a token as described above, then use the following command:
@ -105,7 +105,7 @@ Manifests can have two types. An index (media type of `application/vnd.oci.image
> ```
> </details>
Image manifests (media type of `application/vnd.oci.image.manifest.v1+json` or `application/vnd.docker.distribution.manifest.v2+json`) contain a list layers, each one referencing a blob. These layers are `.tar.gz` files containing the files in the image. The additional metadata is accessible through a separate blob referenced in the `config` section of the manifest.
Image manifests (media type `application/vnd.oci.image.manifest.v1+json` or `application/vnd.docker.distribution.manifest.v2+json`) contain a list of "layers", each of which refers to a blob. In traditional container images, the layers are `.tar.gz` archives representing the files in the root filesystem. Some additional metadata is accessible through a separate blob specified in the `config` section of the manifest.
> [!TIP]
> Try it yourself: authenticate with a token as described above, then use the following command:
@ -148,11 +148,22 @@ Image manifests (media type of `application/vnd.oci.image.manifest.v1+json` or `
## API calls in the "Pull" category
The distribution specification outlines that registries must implement all endpoints in the "Pull" category. This category consists of two endpoints:
The distribution specification requires that registries must implement all endpoints in the "Pull" category. This category contains two endpoints:
- The manifest endpoint is located at `/v2/<name>/manifests/<reference>` and serves manifest documents. These manifests are JSON documents which have a specific content type (e.g. `application/vnd.oci.image.manifest.v1+json`) and the registry is allowed to perform content negotiation based on the `Accept` header the client sends.
- The blob endpoint is located at `/v2/<name>/blobs/<digest>`, containing binary objects based on their digest (checksum). Note that this endpoint may return an HTTP redirect.
The `<name>` part may contain additional `/` characters and must match the regular expression of `[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*`. This means that a name can consist of an arbitrary amount of path parts, up to the length limit of 255 characters for hostname + name. It is worth noting that many registry implementations place additional restrictions on the name, such as needing to include a project ID, group, namespace, etc. and they may disallow additional path parts beyond that.
A `<reference>` can either be a digest or a tag name. Tag names must follow the regular expression of `[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}`.
An OCI `<digest>` can take the form of `scheme:value` and use any checksum algorithm. However, the specification states that `sha256` and `sha512` are standardized and compliant registries should support `sha256`.
> [!NOTE]
> Unfortunately the term "reference" is overloaded with two meanings in the OCI ecosystem, and sometimes refers to the entire address of a tag/digest in a specific repository in a specific registry.
>
> For example, `latest` is a local reference specifying only a tag name but leaving the repository address implied, but `example.com/foo/bar/baz:latest` is a fully-qualified reference that refers to the `latest` tag in the `foo/bar/baz` repository on the `example.com` registry. In this chapter we will use the local meaning of "reference" as described above, but documentation for other software in this ecosystem sometimes uses the other meaning.
> [!TIP]
> Try it yourself: authenticate with a token as described above, then pull a blob from the manifest in the previous example. Observe the `location` header returned in the response, pointing to the actual download location of the blob.
> ```
@ -182,38 +193,33 @@ The distribution specification outlines that registries must implement all endpo
>
> </details>
> [!NOTE]
> The `<name>` part may contain additional `/` characters and must match the regular expression of `[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*`. This means that a name can consist of an arbitrary amount of path parts, up to the length limit of 255 characters for hostname + name. It is worth noting that many registry implementations place additional restrictions on the name, such as needing to include a project ID, group, namespace, etc. and they may disallow additional path parts beyond that. Therefore, we will have to map the provider addresses to the name in a flexible fashion, configurable for the user.
> [!NOTE]
> A `<reference>` can either be a digest or a tag name. Tag names must follow the regular expression of `[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}`.
> [!NOTE]
> An OCI `<digest>` can take the form of `scheme:value` and use any checksum algorithm. However, the specification states that `sha256` and `sha512` are standardized and compliant registries should support `sha256`.
## API calls in the "Push" category
These API calls are similar to the pull category above, but as the name suggest, are intended for publishing manifests and blobs.
These API calls are similar to the pull category above but, as the name suggests, are intended for publishing manifests and blobs.
You can do an upload in two ways:
You can upload a new artifact in two ways:
1. First `POST` to the `/v2/<name>/blobs/uploads` endpoint, then `PUT` the blob contents to the URL indicated in the `Location` header from the first response.
2. Immediately `POST` the blob contents to `/v2/<name>/blobs/uploads/?digest=<digest>` indicating a pre-computed digest.
> [!TIP]
> The benefit of the first method is the ability to perform an upload in chunks using `PATCH` requests. See [the specification for details](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks).
> The benefit of the first method is the ability to perform an upload in chunks using `PATCH` requests. Refer to [Pushing a Blob in Chunks](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks) for more details.
Once the blobs have been uploaded, you can push the manifest that references them. You can push a manifest by sending a `PUT` request to `/v2/<name>/manifests/<reference>`, where the reference should be the tag name the manifest should appear under.
It is worth noting that manifests can reference other manifests in their `subject` field. You can use this, for example, to sign a manifest and attach the signature to the manifest it signed. You can then use the referrers API described in the next section to query it.
Once the blobs have been uploaded, you can push the manifest(s) that refer to them. You can push a manifest by sending a `PUT` request to `/v2/<name>/manifests/<reference>`, where the reference should be the tag name the manifest should appear under.
## API calls in the "Content discovery" category
In addition to the "pull" category outlined above, registries may (but do not necessarily have to) implement the API endpoints in the "Content discovery" category. This category is useful when listing provider versions and consists of two endpoints:
In addition to the "pull" category outlined above, registries may optionally implement the API endpoints in the "Content discovery" category. This category includes two additional endpoints:
- The tag listing endpoint is located at `/v2/<name>/tags/list` and supports additional filtering and pagination parameters. It lists all the tags (a kind of reference) that have manifests.
- The tag listing endpoint is located at `/v2/<name>/tags/list` and supports additional filtering and pagination parameters. It lists all the tags in the selected repository that have associated manifests.
This is useful, for example, to answer the question "which versions of this artifact are available?" in order to implement semantic-versioning-based matching or other similar non-exact selection techniques.
- The referrer listing endpoint is located at `/v2/<name>/referrers/<digest>` and returns the list of manifests that refer to a specific blob.
A manifest can optionally include a `subject` property which refers to another manifest, effectively creating a tree of artifacts where the `subject` property indicates the parent of the current manifest. The referrer listing endpoint then describes the opposite relationship, returning a list of all of the child manifests that refer to the given parent manifest.
Uses of this are still emerging in the ecosystem at the time of writing, but typically it's used for artifacts that serve as post-hoc attestations or other metadata about the parent, such as presenting a signature for the parent manifest that might have been generated by someone other than the parent artifact's author. However, it's notable that some signing mechanisms -- including Cosign -- expect signatures presented as specially-named tags rather than using this API.
> [!WARNING]
> The tag listing endpoint is typically paginated. Client implementations must follow the `Link` header in the response to receive the entire list of tags.
@ -266,12 +272,6 @@ In addition to the "pull" category outlined above, registries may (but do not ne
>
> </details>
This category also includes a way to create references between distinct manifests. As an example, you can attach a digital signature of a manifest to an existing manifest by publishing the signature under a separate manifest, but referring to the manifest it signs. You can do this by pushing the new manifest with a `subject` field that references the original manifest.
Clients can query the `/v2/<name>/referrers/<reference>` endpoint to receive a list of manifests that refer to the current manifest in their `subject` field.
> [!TIP]
> The referrers API has been added in the Distribution spec version 1.1. Prominently, Cosign does not appear to use this API to attach signatures. Instead, Cosign creates a tag named after the checksum of the main manifest and suffix it with `.sig`.
## The `_catalog` extension
@ -281,7 +281,7 @@ Although not standardized in the distribution spec, the `/v2/_catalog` endpoint
Everything above refers to the standard container image layout. However, [ORAS](https://oras.land/) describes how artifacts can be stored in a non-standard layout. ORAS today has wide-ranging support.
ORAS uses a different media type to store the artifact in a layer:
ORAS relies on the `mediaType` property of a layer descriptor to differentiate different kinds of layers, beyond the "differential-tar" format expected for container images. For example, it's possible to declare a layer of type `archive/zip` instead of `application/vnd.docker.image.rootfs.diff.tar.gzip`:
```json
{
@ -323,7 +323,7 @@ ORAS uses a different media type to store the artifact in a layer:
> oras manifest fetch localhost:5000/oras:latest --pretty
> ```
When pushing multiple files with ORAS, each file is stored in a separate layer:
When pushing multiple files to a single tag with ORAS, each file is represented as a separate layer:
```json
{
@ -376,10 +376,12 @@ ORAS can also customize the config manifest using the `--config` option. This wi
}
```
It is worth noting that trying to pull an ORAS image with traditional containerization software may result in unexpected errors [as documented here](https://oras.land/docs/how_to_guides/manifest_config#docker-behaviors).
Traditional container engines were, of course, not built to expect these different layer and config formats and so [they may return "interesting" errors when encountering such manifests](https://oras.land/docs/how_to_guides/manifest_config#docker-behaviors).
It is worth noting that trying to pull an ORAS image with traditional containerization software may result in unexpected errors [as documented here](https://oras.land/docs/how_to_guides/manifest_config#docker-behaviors). ORAS defaults to using a fixed config blob representing an empty JSON object, `{}`, whose media type is `application/vnd.oci.empty.v1+json` and whose digest is always `sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a`. ORAS-aware software understands this convention and skips fetching the known-empty configuration blob altogether, but traditional container engines such as Docker will return errors such as "invalid rootfs in image configuration" if someone tries to use them with such a non-container-image artifact.
> [!NOTE]
> At the time of writing, [ORAS does not support multi-platform images](https://github.com/oras-project/oras/issues/1053). However, it is possible to push manifests manually using the `oras manifest` subcommand.
> At the time of writing [ORAS does not support multi-platform images](https://github.com/oras-project/oras/issues/1053). However, it is possible to push externally-generated manifests directly using the `oras manifest` subcommand.
---

View File

@ -10,10 +10,70 @@ This document is part of the [OCI registries RFC](../20241206-oci-registries.md)
This appendix discusses implementation details related to [installing module packages from OCI registries](5-modules.md).
> [!WARNING]
> This section is still under construction, subject to change based on feedback on the earlier chapters, and may not yet be up-to-date with the latest changes in the earlier chapters.
## OCI Distribution "Getter" for go-getter
> **TODO:** Write this!
OpenTofu currently delegates most of the work of fetching and extracting remote module packages to a third-party upstream library called [go-getter](https://pkg.go.dev/github.com/hashicorp/go-getter). Aside from the special cases of local paths and registry addresses that are handled internally by OpenTofu, all of [OpenTofu's documented module sources](https://opentofu.org/docs/language/modules/sources/) are implemented as some combination of go-getter features.
There are a number of different concepts in go-getter, but the two most relevant to the question of additional module sources are:
- [`Detector`](https://pkg.go.dev/github.com/hashicorp/go-getter#Detector): essentially a preprocessor that takes a raw source address and returns another raw source address, prior to URL parsing.
This is the mechanism we currently use to implement what we document as the "GitHub" and "Bitbucket" source types: they are really just "detectors" that rewrite certain shorthand address patterns into a fully-qualified source address for the generic "Git" getter. For example, a source address like `github.com/example/example` gets rewritten by the GitHub "detector" into `git::https://github.com/example/example.git`.
[Detectors can "stack"](https://github.com/opentofu/opentofu/blob/d2ae0b21ede3dddb92914d3c61b5caa3c7f77db0/internal/getmodules/getter.go#L37-L59) -- the output from an earlier detector is fed as the input into a later detector -- but whichever detector runs last for a particular input _must_ produce something in go-getter's "URL-like" syntax, described in the next item. `Detectors` are therefore most commonly used for translating non-URL-like address schemes into URL-like address schemes.
- [`Getter`](https://pkg.go.dev/github.com/hashicorp/go-getter#Getter) does the main work of fetching and extracting a module package. The detector phase must produce a special extended URL syntax which either explicitly specifies or implies a "getter" from [OpenTofu's table of Getters](https://github.com/opentofu/opentofu/blob/d2ae0b21ede3dddb92914d3c61b5caa3c7f77db0/internal/getmodules/getter.go#L79-L87).
The explicit syntax involves an extra go-getter-specific "scheme" at the front of the address, like the `git::` prefix in the example in the previous point, but if there is no double-colon scheme prefix then go-getter assumes that the URL scheme is also the Getter name, and so e.g. `https://example.com/` would be treated as belonging to the "https" getter.
After go-getter has decided which Getter to use, it trims off any explicit getter-selection prefix and then parses the remainder of the address using standard URL syntax (as implemented by the Go standard library `net/url` package) and passes it to the `Getter.Get` method, along with the filesystem path to the local directory where the package should be extracted.
Our proposed user experience for modules in OCI registries involves using a new URL scheme `oci:`, without any explicit `Getter` selection prefix. To implement that, we must add a new entry `"oci"` to OpenTofu's table of getters. Our proposed address scheme intentionally follows normal URL syntax otherwise, and so we do not need any special `Detector` for this new source address type.
Upstream go-getter does not currently have a `Getter` for the OCI Distribution protocol. From discussion in pull requests in that repository, it does not seem like the upstream is generally accepting entirely new `Getter` implementations. In particular, [a HashiCorp representative commented](https://github.com/hashicorp/go-getter/pull/517#issuecomment-2666366150) that "It can be hard to get changes through in `go-getter`", in response to a proposal to add a new `Getter` for Azure blob storage.
Therefore we will implement our new `Getter` as an OpenTofu-specific implementation inside `package getmodules` to start. At some later date we could potentially try to upstream it, but that is not a priority for this project and so we do not wish to make that a blocker.
As described in [OCI Client for the Module Package Installer](8-auth-implementation-details.md#oci-client-for-the-module-package-installer), this `Getter` will be written using a dependency-inversion style where it recieves a glue function from `package main` that integrates it with our cross-cutting OCI repository authentication design:
```go
func NewOCIGetter(
getRegistryClient func(ctx context.Context, registryDomainName, repositoryPath string) (*orasregistry.Registry, error),
) getter.Getter
```
Our initial implementation of this getter will therefore rely on the ORAS-Go library's OCI Distribution client, which provides us with the necessary building-blocks for finding and fetching a module package:
- Resolve a tag into a manifest digest, if the address doesn't specify a digest explicitly using the `digest` argument.
- Fetch a manifest given its digest, which will then in turn tell us the digest of the `.zip` package blob.
- Fetch a blob given its digest, which our `Getter` will use to retrieve the `.zip` file before extracting it into the target directory.
Go-getter also has a further concept [`Decompressor`](https://pkg.go.dev/github.com/hashicorp/go-getter#Decompressor) which in principle allows the use of any one of [a selection of different archive formats](https://github.com/opentofu/opentofu/blob/d2ae0b21ede3dddb92914d3c61b5caa3c7f77db0/internal/getmodules/getter.go#L63-L77) for the final package payload. In practice this is only really used for the HTTP (or "HTTP-like") getters in OpenTofu, to implement [Fetching archives over HTTP](https://opentofu.org/docs/language/modules/sources/#fetching-archives-over-http). Further, the "decompressor" concept is implemented as something independent of any specific getter to be activated as part of the source address syntax, and so it's not a suitable abstraction for situations like OCI Distribution where our getter wants to detect from the manifest which archive format is in use, rather than that being controlled by an independent query string argument.
We wish to keep our OCI image layouts relatively constrained to start, and so we specified that a module package distributed via the OCI Distribution protocol _must_ be a `.zip` archive, and therefore its descriptor must have `mediaType` set to `archive/zip`. To implement zip extraction we'll write our `OCIGetter` to _directly_ instantiate the upstream [`getter.ZipDecompressor`](https://pkg.go.dev/github.com/hashicorp/go-getter#ZipDecompressor) and call it inline as an implementation detail of the final step of extracting the `.zip` archive blob into the target directory. Reusing go-getter's "decompressor" will ensure that our treatment of the `.zip` package will be exactly equivalent to how today's OpenTofu would treat a `.zip` package retrieved using the HTTP getter (etc), which includes some safety features like detecting and rejecting file paths that seem to traverse up out of the target package directory.
> [!NOTE]
> The above signature for `NewOCIGetter` is intended to be illustrative rather than concrete. In practice it may end up taking a single argument that is an implementation of some interface with a method like the function signature shown, but that's a level of design detail we will consider during the implementation phase, with consideration to whether it turns out to be shareable with the similar dependency inversion design we'll be using for the provider installer.
> [!NOTE]
> We could potentially choose to support other blob media types in future versions if we learn of a good reason to do so. If we decide to do that, the most likely design is for `OCIGetter` to have its own table mapping from `mediaType` value to `getter.Decompressor`, along with a priority rule for choosing one blob to use when there are multiple of supported formats.
>
> Our existing table of "decompressors" is keyed by filename suffix rather than by media type -- i.e. `.zip` rather than `archive/zip` -- and so it is not suitable to reuse for matching descriptors in an OCI manifest.
>
> However, this proposal does not intend to constrain those future design details at all. Our initial implementation will ignore any blobs that are not `archive/zip` and will fail with a suitable error message if none use that media type. The hypothetical future project that adds a second supported media type can then propose answers to the relevant questions working within those constraints.
## Package Checksums and Signing
Today's OpenTofu has no general-purpose mechanism for verifying module package checksums, fetching and checking signatures, or "remembering" checksums in the dependency lock file. Although we have heard requests for all of those mechanisms, they are explicitly not in scope for this RFC and should instead be tackled in a cross-cutting way (across all supported source types) in a separate later proposal.
A typical compromise made with today's OpenTofu is to use the "git" getter with a `ref` argument set to a specific Git commit, which therefore provides at least a SHA-1 checksum to verify the resulting content against. SHA-1 is no longer considered cryptographically secure, but the Git community is gradually moving toward using SHA-256, which OpenTofu will automatically benefit from once it becomes more widely deployed.
The OCI Repository source address syntax offers the `digest` argument which provides a similar mechanism: it specifies a checksum of the manifest of the package to be fetched, and the manifest in turn contains a checksum of the final `.zip` blob, and so a source address using that argument (rather than `tag`) can guarantee to succeed installation only if the retrieved manifest actually matches the digest.
> [!NOTE]
> Notwithstanding the earlier remark about treating signature verification as a general concern across all source types, it _is_ potentially possible that a future version of OpenTofu could use [the OCI Distribution referrers API](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers) to automatically discover cryptographic signatures associated with a manifest in an OCI-registry-specific way. However, that is explicitly not in scope for this initial project, and if we decide to pursue it in a later project we should carefully consider the tradeoffs of solving this in an OCI-specific way vs. in a manner that is applicable to other OpenTofu module source types.
>
> Either way, it will require some extensions to our overall module installer API to include some way for the installer to report the outcome of signature verification work so that `tofu init` can display the signing key information in a similar way as we currently do for providers installed from a provider registry. This initial project will not include any changes to the overall module installer API, instead focusing only on providing a new implementation of the existing API.
---

View File

@ -8,44 +8,44 @@ This document is part of the [OCI registries RFC](../20241206-oci-registries.md)
---
We have run a survey on the OCI feature among the OpenTofu community, which attracted 103 responses. This document discusses this survey and the conclusions we draw from it.
We have run a survey on the OCI feature among the OpenTofu community, which attracted 103 responses. This document discusses this survey and the conclusions we drew from it.
> [!WARNING]
> The survey analysis is still pending verification, these numbers may change.
> The survey analysis is still pending verification, and so these numbers may change.
## 82% want to use private provider registries
96 out of 103 respondents (93%) indicated that they are interested in using OCI for provider registries. Out of these 85 (82%) indicated that they would be interested in using OCI in either to mirror a public provider (66 responses, 64%) or publish a private provider (63 responses, 61%). 8 respondents (8%) indicated that they are purely interested in a public provider ecosystem based on OCI and do not have a use case for a private OCI registry. However, 13 respondents (13%) indicated that they are authoring a public provider and would like to publish their provider in an OCI registry.
96 out of 103 respondents (93%) indicated that they are interested in using OCI for provider distribution. Out of these 85 (82%) indicated that they would be interested in using OCI either to mirror an existing public provider (66 responses, 64%) or publish a private provider (63 responses, 61%). 8 respondents (8%) indicated that they are purely interested in a public provider ecosystem based on OCI and do not have a use case for a private OCI registry. However, 13 respondents (13%) indicated that they are authoring a public provider and would like to publish their provider in an OCI registry.
One respondent pointed our attention to the fact that provider documentation would be an issue with OCI in the additional notes. (Thank you, we didn't think of this as an issue!) This is due to the fact that the OpenTofu Registry currently indexes documentation separately and provider authors don't publish documentation packages. This will be something we need to address in the future.
One respondant noted that a generic OCI registry would not automatically generate and publish provider documentation as the main OpenTofu registry does. (Thank you!) Therefore if it becomes popular in future to publish providers or modules _exclusively_ in OCI registries then we will need to somehow address that.
As far as the current solutions are concerned, the responses gathering more than one response were:
We also asked what solutions respondants were already using for provider distribution today. The answers with more than one response were:
- **Using the OpenTofu registry:** 56 responses
- **Using the default OpenTofu public registry:** 56 responses
- **Using a private provider registry:** 27 responses
- **Using an alternative public registry:** 18 responses
- **Using a mirror directory:** 22 responses
- **Using a filesystem mirror directory:** 22 responses
- **Manually deploying providers:** 14 responses
- **Not using OpenTofu today:** 13 responses
- **Using a provider mirror server:** 11 responses
- **Using a network mirror server:** 11 responses
## 90% want to use private module registries
93 out of 103 respondents (90%) indicated that they would be interested in using OCI for modules. Out of these, 87 (84%) indicated that they would be interested in using a private registry to either publish modules in a private registry (82 respondents, 79%) or mirror public modules (61 respondents, 59%). Only 4 respondents (4%) indicated that they would be purely interested in using a public OCI module ecosystem. 17 respondents (17%) indicated that they are creating publicly available modules and would be interested in publishing them to an OCI registry.
93 out of 103 respondents (90%) indicated that they would be interested in using OCI for module distribution. Out of these, 87 (84%) indicated that they would be interested in using a private registry to either publish modules in a private registry (82 respondents, 79%) or mirror public modules (61 respondents, 59%). Only 4 respondents (4%) indicated that they would be purely interested in using a public OCI module ecosystem. 17 respondents (17%) indicated that they are creating publicly available modules and would be interested in publishing them to an OCI registry.
When it comes to the current solutions in use, the answers with more than one response were:
We also asked what solutions respondants were already using for module distribution today. The answers with more than one response were:
- **Using a private Git repository:** 72 responses
- **Using the OpenTofu Registry:** 37 responses
- **Using the default OpenTofu public Registry:** 37 responses
- **Using a public Git repository:** 37 responses
- **Using a private OpenTofu/Terraform registry:** 32 responses
- **Using an alternative public registry:** 19 responses
- **Using a shared drive:** 9 responses
- **Using a network filesystem:** 9 responses
- **Not using modules with OpenTofu:** 7 responses
## 85% want to use a security scanner
In our survey, we asked our community what security scanning tools they would like to use with OCI. 88 out of 103 respondents have answered this question and indicated the following tools would be of help (responses with more than one vote):
In our survey, we asked our community which security scanning tools they would like to use. 88 out of 103 respondents have answered this question and indicated the following tools would be of help (responses with more than one vote):
- **Trivy**: 47 responses
- **TFLint:** 34 responses
@ -61,11 +61,11 @@ In our survey, we asked our community what security scanning tools they would li
## 33% use an airgapped setup
What we found most surprising was that 35 respondents (33%) indicated that they have a some level of air-gapped infrastucture. An air-gapped infrastructure necessitates the mirroring of all artifacts used for the deployment.
What we found most surprising was that 35 respondents (33%) indicated that they have a some level of air-gapped infrastucture. We use "air-gapped" to mean any situation where OpenTofu is running in an environment where it is unable to access (or forbidden from accessing by policy) public registry services, and so this requires creating a local mirror of all needed dependencies.
## A wide range of registry implementations
We have also asked our community about the registries that are interesting. Here is the distribution of responses with more than one response:
We have also asked our community about which OCI registries they might like to use with OpenTofu. The following are responses with more than one vote:
- **GitHub (ghcr.io):** 57 responses
- **AWS ECR:** 54 responses
@ -85,9 +85,9 @@ We have also asked our community about the registries that are interesting. Here
## Tooling
We asked what kind of tools our community would like to use for working with OCI artifacts when publishing them. 27 respondents indicated that they would like to use their existing Podman/Docker/Containerd to push artifacts. In contrast, 24 respondents indicated that they would like OpenTofu to have its own built-in tooling. Although not present in the survey answers, 3 respondents indicated that they would like to use ORAS or built-in tooling to publish OCI artifacts. 24 respondents indicated that they have no strong preference as long as the tooling works in GitHub Actions.
We asked which tools our community would like to use for generating and publishing OCI artifacts for OpenTofu. 27 respondents indicated that they would like to use Podman/Docker/Containerd workflows to push artifacts. In contrast, 24 respondents indicated that they would like OpenTofu to have its own built-in tooling. Although not present in the survey answers, 3 respondents indicated that they would like to use ORAS or built-in tooling to publish OCI artifacts. 24 respondents indicated that they have no strong preference as long as the tooling works in GitHub Actions.
When it comes to credential storage, a majority of respondents to that question (36 responses) indicated that they are using some sort of cloud integration for their OCI credentials. 19 responses indicated that their users are using the Docker/Podman credentials helpers. 16 respondents are using plain-text storage for their OCI credentials today. 2 respondents have indicated that they are using Kubernetes secrets to store OCI credentials. As far as preferences go, 55 respondents indicated that they would like OpenTofu to reuse existing Container ecosystem credentials, whereas 26 respondents indicated that OpenTofu should use its own credentials. Out of these, 12 respondents indicated that they are happy with both solutions.
When it comes to credential storage, a majority of respondents to that question (36 responses) indicated that they are using some sort of cloud integration for their OCI credentials. 19 responses indicated that their users are using the Docker/Podman credential helpers. 16 respondents are using local cleartext storage for their OCI credentials today. 2 respondents responded that they are using Kubernetes secrets to store OCI credentials. As far as preferences go, 55 respondents indicated that they would like OpenTofu to reuse existing Container ecosystem credentials, whereas 26 respondents indicated that OpenTofu should use its own credentials. Out of these, 12 respondents indicated that they are happy with both solutions.
## A note of gratitude
@ -95,7 +95,7 @@ In total, we have received a larger number of responses than we expected, equall
In the motivation for OCI many responses indicated that it would solve a real pain-point when it comes to running private distribution servers or CI systems with a large number of jobs requiring bandwidth for downloads. According to the answers, OCI is widely deployed and makes in-house distribution, caching and mirroring much easier.
So, thank you again to all who participated in the survey.
Thank you again to all who participated in the survey. While our first iteration of these features as described in this RFC will not satisfy all of the responses, we have made an intentional effort to reserve opportunities to extend the design to meet additional use-cases in future releases.
---

View File

@ -10,72 +10,78 @@ This document is part of the [OCI registries RFC](../20241206-oci-registries.md)
## Provider addresses are virtual
Currently, providers in OpenTofu are addressed by a virtual address in the format of `HOSTNAME/NAMESPACE/TYPE`, where `HOSTNAME` defaults to `registry.opentofu.org` and `NAMESPACE` defaults to `hashicorp` for historic reasons.
Providers in OpenTofu are addressed by a virtual address in the format of `HOSTNAME/NAMESPACE/TYPE`, where `HOSTNAME` defaults to `registry.opentofu.org` and `NAMESPACE` defaults to `hashicorp` for historic reasons. The hostname identifies the so-called "origin registry" of the provider, which is typically the location where the provider author announces new releases.
However, this address is independent of the actual download URL, which can be anywhere. When OpenTofu needs to download a provider (unless configured with a mirror), it consults the [Remote Service Discovery endpoint](https://opentofu.org/docs/internals/remote-service-discovery/) on the provided hostname and then uses the [Provider Registry Protocol](https://opentofu.org/docs/internals/provider-registry-protocol/) to obtain the correct download URL.
However, a provider source address is intentionally independent of the actual download URL. By default, OpenTofu contacts the origin registry using [the Remote Service Discovery protocol](https://opentofu.org/docs/internals/remote-service-discovery/) and the [Provider Registry Protocol](https://opentofu.org/docs/internals/provider-registry-protocol/) to find the "official" release package locations. OpenTofu allows an operator to reconfigure the installation strategy for some or all provider source addresses, in which case the hostname in the source address serves only as part of the provider's unique identifier for OpenTofu tracking purposes, while the distribution packages can be discovered and installed from an entirely separate location. For example, OpenTofu uses the source address to "remember" which provider manages each resource in an OpenTofu state snapshot, and so the provider's source address must remain consistent between plan/apply rounds.
It is worth noting that, in contrast to modules, provider addresses have no protocol part, such as `oci://`.
OpenTofu modules use this virtual address syntax to declare which providers they need _without_ specifying how they are to be installed. This then allows operators to choose to distribute providers from organization-local mirrors instead of origin registries without needing to modify the source code of the modules themselves. It is for this reason that provider source addresses to not include any part representing the installation protocol, and are therefore _not_ URLs.
In our survey, 64% of respondents have indicated interest in using publicly available providers without using the registry. In parallel, 13% of respondents have indicated that they would be interested in publishing a provider in a public OCI registry.
In our survey, 64% of respondents have indicated interest in using publicly available providers without using the registry, while 13% of respondents have indicated that they would be interested in publishing a provider in a public OCI registry. A similar number of respondents have indicated that they would like to mirror a provider or create a provider for their internal organization.
In parallel, a similar number of respondents have indicated that they would like to mirror a provider or create a provider for their internal organization.
To preserve the ability for an operator to unilaterally change the installation method for any provider, we must continue to ensure that no part of the source address syntax forces the use of the OCI Distribution protocol or forces OpenTofu to install any provider from a specific OCI registry. We _could_ potentially allow some domains to have a different _default_ installation behavior using OCI Distribution protocol instead of the OpenTofu Provider Registry protocol, but it must remain possible for the operator to mirror this provider to another location without changing its source address.
Changing the provider address understanding would result in numerous changes in the OpenTofu and other codebases, and affect third party tools rely on this understanding. It would also affect the state file format, as well as the JSON output.
Since introducing a new _default_ installation method for certain provider addresses would be profound change, we intend to be cautious: our first release will focus on introducing OCI Registries only as a new kind of provider _mirror_, which operators can opt into with [CLI configuration settings](https://opentofu.org/docs/cli/config/config-file/#provider-installation). For more information, refer to [4-providers.md](Providers).
Alternatively, we could reserve a specific subdomain owned by the OpenTofu project to represent the intent to install directly from an OCI registry, giving source addresses like `ghcr.io.example.com/namespace/name` (assuming that `example.com` were the special subdomain, which is of course just a placeholder). This, however, opens up several questions, for example:
1. How do we convert a virtual provider address into an OCI address? Some OCI registries need various prefixes, etc.
2. How does such a change affect third party tools?
Since adding support for public providers via OCI would be a profound change, we opt to proceed with caution. We will implement using OCI for providers in a private setting first. In other words, we will treat OCI for providers the same way as the Provider Mirror Protocol and you will need to add [a CLI configuration option](https://opentofu.org/docs/cli/config/config-file/#provider-installation) to your `.tofurc` file, which we explore in detail in the [providers section of this RFC](providers.md).
The OpenTofu Provider Registry Protocol will remain the only available _default_ installation method for now, although we will consider options for using OCI Registries as a new kind of "origin registry" in future releases, once we've learned from the experience of implementing and releasing the new opt-in mirror type.
## OCI layout
The most important consideration for us was the layout of the data in the OCI registry. As described [in the primer](1-oci-primer.md), OCI registries are very flexible how they store data.
A significant consideration is the exact layout of the manifests and blobs for our new artifact types in the OCI registry. As described in [OCI Primer](1-oci-primer.md), OCI registries are very flexible how they store data.
Originally, we considered that we could store both providers and modules in a traditional container image layout. This would have allowed you to use your existing Docker/Podman tooling to publish their images by creating a `Dockerfile`/`Containerfile` with the following contents:
Originally we aimed to represent both providers and modules using the same "differential-tar"-style layout used for traditional container images, including the typical `mediaType` values and blob formats used for container images. This would've allowed constructing OpenTofu artifacts using the same tools commonly used for container images, such as `docker build` with a `Dockerfile`, like this:
```Dockerfile
FROM scratch
ADD * /
LABEL "org.opentofu.artifact-type"="provider"
```
However, there are several key reasons for deciding against this layout:
However, after some prototyping we abandoned this approach primarily because it would cause a provider package mirrored in an OCI registry to have a different checksum than its upstream from a traditional OpenTofu provider registry. OpenTofu verifies provider packages against checksums signed by the provider author which are calculated from the `.zip` archive used for distribution, rather than the contents of that archive. These checksums are recorded in the [dependency lock file](https://opentofu.org/docs/language/files/dependency-lock/) to allow for a workflow where, for example, the lock file is originally generated from metadata in the provider's origin registry but then later used to verify copies of those packages in a mirror used in an air-gapped production environment.
1. **It changes provider checksums**<br />OpenTofu today records the checksums of all providers it sees in the `.terraform.lock.hcl` file. These checksums can have two formats: the `h1` (container-agnostic directory hash) and the `zh` (zip file hash) format. While the former would be suitable for hashing container images, hashes today are almost universally the of the latter format. For legacy reasons, provider authors publish checksums of their `.zip` files in the SHA256SUMS file when releasing providers and sign the checksums file with their GPG key. Fortunately for us, OCI registries also use SHA256 checksums as blob identifiers, so storing the ZIP file in a blob in OCI will guarantee that the checksum doesn't change even when switching from the OpenTofu Registry to a mirrored OCI registry. In contrast, a container image-like layout would mean you have to run `tofu providers lock` to update the checksums in your `.terraform.lock.hcl` and your lock file would now be exclusive to your OCI mirror.
2. **Supporting layers adds complexity**<br />Supporting the diff-tar layer format adds complexity to the codebase and increases the resource consumption of the download. Downloading a ZIP-blob instead allows us to reuse much of the provider-package-handling code already in place in OpenTofu today.
Although the OpenTofu dependency lock file format supports checksums generated in a variety of different ways, the mirror-verification workflow works best when all sources can agree on a single checksum scheme to use, because otherwise operators must manually encourage OpenTofu to generate additional checksum schemes (via local checksum calculation) using [`tofu providers lock`](https://opentofu.org/docs/cli/commands/providers/lock/).
Therefore, we have decided to use a layout that does not indicate a container image and follows the [ORAS](https://oras.land) conventions with added multi-platform support. Refer to the [Providers](4-providers.md) and [Modules](5-modules.md) documents for details on the respective artifact layouts.
An OCI digest of a zip package blob using the `sha256:` scheme is effectively the same as OpenTofu's zip-checksum format, albeit with a slightly different syntax. Therefore we have decided to use the `archive/zip` media type for the blobs in OpenTofu's artifact formats so that OpenTofu can directly compare an OCI-style digest of such a blob with a zip-checksum previously recorded in the dependency lock file, or vice-versa.
Secondarily, reusing the zip format for OpenTofu's artifacts means that we can rely on the existing code in OpenTofu for extracting such archives for use as either provider or module packages, thereby reducing the total amount of code we'll need to maintain and making it considerably less likely that a package distributed via the OCI Distribution protocol will produce a different extracted package on disk to one obtained by other means.
Refer to [Providers](4-providers.md) and [Modules](5-modules.md) for more details on the specific artifact layouts we intend to use.
## Software Bill of Materials (SBOM)
The decision to use a custom image layout as described above profoundly impacts security scanners, which a [large number of our users want to use](2-survey-results.md). We have tested several of the popular security scanners with varying degrees of success.
The decision to use a custom image layout as described in the previous section impacts security scanners, which a [large number of our users want to use](2-survey-results.md). We have tested several of the popular security scanners with varying degrees of success.
The popular scanners supporting container images out of the box, such as [Trivy](https://trivy.dev/), only directly support the default container image-like layout and do not support custom artifact types, such as the one uploaded with [ORAS](https://oras.land/). Other popular tools, such as TFLint, do not support container images at all and need to be configured manually.
The popular scanners supporting container images out of the box, such as [Trivy](https://trivy.dev/), only directly support the differential-tar layer format used for container images, and do not currently support custom artifact types. Some tools can support single-image artifacts when they use the container image layout, but do not yet support multi-platform index manifests. Other popular tools, such as TFLint, do not directly support OCI Distribution at all.
However, tools in this space have varying degrees of support for indirect scanning via Software Bill of Materials (SBOM) manifests, which is a growing new design that effectively separates the problem of taking inventory of dependencies from the problem of detecting whether those dependencies have known vulnerabilities. This means that we can provide a means for a provider developer to publish an SBOM for their provider and let the security scanners work with that instead of attempting to construct one themselves by directly inspecting the OCI artifact.
However, tools in this space have varying degrees of support for indirect scanning via Software Bill of Materials (SBOM) manifests, which is a growing new design that effectively separates the problem of taking inventory of dependencies from the problem of detecting whether those dependencies have known vulnerabilities. This means that we could provide a means for a provider developer to publish an SBOM for their provider and let the security scanners work with that instead of attempting to construct one themselves by directly inspecting the OCI artifact.
To reduce scope for the initial implementation we do not intend to generate or use SBOM artifacts initially. However, we are considering extending the OpenTofu Provider Registry and Module Registry protocols to be able to return additional links to SBOM artifacts in future, at which point it would become possible to copy those artifacts into an OCI registry along with the packages they relate to, and thus make that information available to any OCI-registry-integrated security scanners that have SBOM support.
At the time of writing we have started discussing possible future SBOM functionality in [Pull Request #2494](https://github.com/opentofu/opentofu/pull/2494). Our initial release will prioritize the fundamentals of hosting provider and module packages in OCI registries _at all_, and so those whose interest in OCI-based distribution is motivated primarily by security scanner integration will need to either wait for a future release or generate and upload SBOMs directly to their registry.
At the time of writing we have started discussing possible future SBOM functionality in [Pull Request #2494](https://github.com/opentofu/opentofu/pull/2494). Our initial release will prioritize the fundamentals of hosting provider and module packages in OCI registries _at all_, and so those whose interest in OCI-based distribution is motivated primarily by security scanner integration will need to either wait for a future release or generate and upload SBOMs to their registry separately.
## Artifact signing
While not a question in the survey, several respondents have taken the time to express that artifact signing is an important consideration to them.
While not a question in the survey, several respondents mentioned that artifact signing is an important consideration for them.
Today, OpenTofu providers are signed with GPG keys and [there is an open issue about supporting Sigstore/Cosign](https://github.com/opentofu/opentofu/issues/307), which is blocked on the availability of a stable Go library to do so. Another project worth some consideration is the [Notary Project](https://notaryproject.dev/) with similar aims to Sigstore/Cosign.
Today, OpenTofu providers are signed with GPG keys and [there is an open issue about supporting Sigstore/Cosign](https://github.com/opentofu/opentofu/issues/307), which is blocked on the availability of a stable Go library to do so.
Modules are currently not signed in OpenTofu, which is a separate question to address. This consideration will, therefore, only address providers.
Module packages are currently not signed in OpenTofu at all, which is a separate question to address. This consideration therefore applies primarily to providers.
For providers, the OpenTofu Registry, an authority independent of the provider author, serves the role of verifier. If the OpenTofu Registry has verified a GPG key as belonging to an author, OpenTofu accepts it as valid. The same is true for third party registries: the registry holds the public GPG keys and provides them for verification, which is independent of the provider download URL. A malicious upload to the download URL would result in an invalid download unless the attacker can obtain the GPG key.
For providers the OpenTofu Registry, an authority independent of the provider author, serves the role of verifier. If the OpenTofu Registry has verified a GPG key as belonging to an author, OpenTofu accepts it as valid. The same is true for third party registries: the registry holds the public GPG keys and provides them for verification, which is independent of the provider download URL. A malicious upload to the download URL would result in an invalid download unless the attacker can obtain the GPG key.
With OCI, however, there is no such central authority. We could implement publishing the SHA256SUMS file and its signature as part of the manifest, but this would not provide any additional benefit as OpenTofu would have to accept any GPG signature as valid. Alternatively, users would have to set up a list of valid GPG public keys, adding the burden of key management to the user.
With OCI, however, there is no default central authority. We could implement publishing the SHA256SUMS file and its signature as part of the manifest, but this would not provide any independent information about which keys are allowed to sign a given provider. Alternatively, users would have to configure their own list of accepted GPG public keys.
Alternatively, we could also support Sigstore/Cosign for providers as well, but this is blocked on the availability of a stable Go library and is also [tricky to run in an air-gapped environment](https://blog.sigstore.dev/sigstore-bring-your-own-stuf-with-tuf-40febfd2badd/), which is something that over 30% of respondents indicated running.
There is also still an apparent lack of consensus in the OCI ecosystem about how to generate, publish, discover, and verify signatures. For example, [Cosign uses a tag naming convention](https://docs.sigstore.dev/cosign/signing/signing_with_containers/#signature-location-and-management), while [Notary Project](https://github.com/notaryproject/specifications/blob/main/specs/signature-specification.md#oci-signatures) represents similar information using the OCI "Referrers" mechanism to create a two-way relationship between signature and target artifact.
One of the main goals of supporting OCI is to ease the maintenance burden, not add to it. This is also something that many respondents indicated in their responses when we asked about the reasons for wanting OCI. Running a Sigstore infrastructure or performing manual key management is contrary to this goal.
Since the path forward on signing is currently not clear, we will defer signature verification to a later release. This is consistent with the lack of support for signing in the [Provider Network Mirror Protocol](https://opentofu.org/docs/internals/provider-network-mirror-protocol/), and is justified by mirrors always being explicitly configured by the OpenTofu operator and thus assumed to be trusted by the operator to provide correct provider packages.
Since the path forward on signing is currently not clear, we will defer signing to a later release. This is consistent with the lack of support for signing in the [Provider Network Mirror Protocol](https://opentofu.org/docs/internals/provider-network-mirror-protocol/).
Although our first release will not offer any specific solution for provider packages being signed directly by their authors, it will be possible for those who are populating OCI registries to act as provider mirrors or module package sources to generate their _own_ signatures and publish them as additional artifacts that the registry would consider as "referrers" of the main artifact. Such signatures could, for example, represent that a particular person or team within an organization has inspected and approved a dependency for use within that organization, and a separate tool outside of OpenTofu could then scan the repository and draw attention to any artifacts that are not signed in this way. We don't intend to provide any specific tools for this use-case, because it's in support of a process used _alongside_ OpenTofu rather than _within_ OpenTofu, but the ability to represent post-hoc signatures from inside an organization is a general OCI Registry capability that organizations will now be able to benefit from for OpenTofu alongside other OCI registry users, using artifact-format-agnostic tools.
> [!NOTE]
> Although we do not intend to implement any specific design for publishing and verifying provider artifact signatures in the first release, we _have_ tried some prototypes of different strategies to develop confidence that we will be able to successfully retrofit a suitable design later.
>
> The question of which entity serves as "verifier" for the provider signing keys is a policy question that remains to be answered, but if we assume that there will be _some_ reasonable answer to that question then a validly-signed OCI artifact manifest provides equivalent information to the metadata OpenTofu currently obtains from its Provider Registry Protocol: the public key used to sign, and a signature that covers the SHA256 checksums of the provider's official `.zip` packages for a particular version across all platforms.
>
> Therefore a future implementation of verification will be able to integrate into OpenTofu's existing model of provider signature verification with only minimal changes to the protocol-agnostic infrastructure, with most of the work being performed in the OCI-specific "provider source" implementation.
---

View File

@ -8,13 +8,15 @@ This document is part of the [OCI registries RFC](../20241206-oci-registries.md)
---
As stated in [Design Considerations](3-design-considerations.md), in this iteration we will focus on serving the provider mirroring use case.
As stated in [Design Considerations](3-design-considerations.md), in this iteration we will focus only on the provider "mirroring" use case.
This means that the first version will be focused on using an OCI registry as an alternative source for a provider whose origin is a traditional OpenTofu provider registry. It will also be technically possible to manually construct and publish provider package artifacts for in-house providers by following OpenTofu's expected conventions for the manifest formats, but we intend to support that use-case better in a subsequent release.
This means that the first version will be focused on using an OCI registry as an alternative source for a provider whose origin is a traditional OpenTofu provider registry. It will also be technically possible to manually construct and publish provider package artifacts for in-house providers by following OpenTofu's expected conventions for the manifest formats, but we intend to support that situation better in a subsequent release.
## Configuring a provider in OCI
## Configuring an OCI-Registry-based Provider Mirror
In order to configure OCI as a provider source in OpenTofu, you will have to modify your [OpenTofu CLI Configuration](https://opentofu.org/docs/cli/config/config-file/). Specifically, you will need to add a `provider_installation` block with at least one `oci_mirror` installation method:
OpenTofu has broadly two categories of provider installation method: "direct" and "mirror". The "direct" installation strategy attempts to find a provider using the hostname portion of its source address, which currently requires that hostname to offer an OpenTofu Provider Registry. The various "mirror" methods instead use the hostname in the source address only as part of a provider's unique identifier, with the physical distribution packages hosted in a secondary location typically operated privately within an organization. For more information, refer to [Provider Installation](https://opentofu.org/docs/cli/config/config-file/#provider-installation).
In order to configure an OCI registry as a provider installation method in OpenTofu, operators must change their [OpenTofu CLI Configuration](https://opentofu.org/docs/cli/config/config-file/). Specifically, they will need to write a `provider_installation` block with at least one `oci_mirror` block:
```hcl
provider_installation {
@ -34,13 +36,13 @@ provider_installation {
In this case, provider addresses matching the `include` arguments would be redirected to the specified OCI registries. Any provider that does not belong to one of the two configured hostnames would still be installed from its origin registry as normal, due to the (optional) inclusion of the `direct` installation method.
The templating is required because OCI registry addresses work differently to OpenTofu provider addresses and some registries require specific prefixes. The `repository_template` argument must include a substitution for each component of the provider source address that is a wildcard in the `include` argument:
The templating is required because OCI registry addresses work differently to OpenTofu provider addresses and some registries require specific repository path layouts. The `repository_template` argument must include a substitution for any component of the provider source address that is written as a wildcard (`*`) in the `include` argument:
* `${hostname}` represents the hostname (which defaults to `registry.opentofu.org` for source addresses that only have two parts, like `hashicorp/kubernetes`)
* `${namespace}` represents the namespace (the `foo` in `example.net/foo/bar`)
* `${name}` represents the provider name (the `bar` in `example.net/foo/bar`)
In practice, most commonly-used providers today belong to the `registry.opentofu.org` hostname, which the public registry run by the OpenTofu project. Therefore to support "air-gapped" systems (which cannot directly connect to `registry.opentofu.org`) an organization would need to copy the packages for providers they use from their origin locations returned by the OpenTofu registry into a systematic naming scheme under an OCI registry and then use an `oci_mirror` block that includes providers matching `registry.opentofu.org/*/*`:
In practice, most commonly-used providers today belong to the `registry.opentofu.org` hostname, which the public registry run by the OpenTofu project. Therefore to support "air-gapped" systems (which cannot directly connect to `registry.opentofu.org`) an organization could copy the packages for providers they use from their origin locations returned by the OpenTofu registry into a systematic repository naming scheme under an OCI registry and then use an `oci_mirror` block that includes providers matching `registry.opentofu.org/*/*`:
```hcl
provider_installation {
@ -51,7 +53,7 @@ provider_installation {
}
```
With this CLI configuration, when initializing a module that depends on the `hashicorp/kubernetes` provider, OpenTofu would install the provider from the `opentofu-provider-mirror/hashicorp_kubernetes` repository in the OCI registry running at `example.com`, instead of contacting `registry.opentofu.org` directly. In this configuration, `registry.opentofu.org` is serving only as part of the provider's unique identifier and not as a physical network location. This is an OCI Distribution-compatible equivalent of OpenTofu's existing `network_mirror` installation method, which currently uses [an OpenTofu-specific protocol](https://opentofu.org/docs/internals/provider-network-mirror-protocol/).
With this CLI configuration, when initializing a module that depends on the `hashicorp/kubernetes` provider, OpenTofu would install the provider from the `opentofu-provider-mirror/hashicorp_kubernetes` repository in the OCI registry running at `example.com`, instead of contacting `registry.opentofu.org` directly. `registry.opentofu.org` is serving only as part of the provider's unique identifier and not as a physical network location. This is an OCI Distribution-compatible equivalent of OpenTofu's existing `network_mirror` installation method, which currently uses [an OpenTofu-specific protocol](https://opentofu.org/docs/internals/provider-network-mirror-protocol/).
> [!TIP]
> For example, Amazon ECR registries have the format of *aws_account_id*.dkr.ecr.*region*.amazonaws.com/*repository*:*tag*.
@ -71,11 +73,13 @@ With this CLI configuration, when initializing a module that depends on the `has
## Storage in OCI
OpenTofu takes some inspiration from how [ORAS](1-oci-primer.md#oras) stores files, but with a few key differences. At the time of writing [ORAS is intending to add support for multi-platform index manifests](https://github.com/oras-project/oras/pull/1514), and we aim to be compatible with that proposal.
OpenTofu takes some inspiration from how [ORAS](1-oci-primer.md#oras) stores artifacts, but at the time of writing [ORAS support for multi-platform index manifests is in progress](https://github.com/oras-project/oras/pull/1514) and so we will implement a compatible index manifest layout directly within OpenTofu:
1. Each OpenTofu provider OS and architecture (e.g. linux_amd64) will be stored as a ZIP file directly in an OCI blob. OpenTofu will not use tar files as it would be typical for a classic container image.
2. Each provider OS and architecture will have an image manifest with a single layer with the `mediaType` of `archive/zip` that is expected to be a direct copy of the provider developer's official distribution package for that platform.
3. The main manifest of the artifact will be an index manifest, containing separate entries for each OS and architecture supported by that release of the provider. Additionally, the main manifest must declare the `artifactType` attribute as `application/vnd.opentofu.provider` in order for OpenTofu to accept it as a provider image.
1. Each OpenTofu provider OS and architecture (e.g. linux_amd64) will be stored as a `.zip` file directly in an OCI blob. OpenTofu will not use tar files as would be typical for a container image.
2. Each provider OS and architecture must have an image manifest with a single layer with the `mediaType` of `archive/zip` that is expected to be a direct copy of the provider developer's official distribution package for that platform.
3. The main manifest of the artifact must be an index manifest, containing separate entries for each OS and architecture supported by the associated release of the provider. The main manifest have the `artifactType` property set to `application/vnd.opentofu.provider` in order for OpenTofu to accept it as a valid provider image.
The OCI index manifest format coincidentally uses the same operating system and CPU architecture codes as OpenTofu does, because both projects inherited that naming scheme from the toolchain of the Go programming language. Therefore, for example, the platform called `linux_amd64` in OpenTofu is represented in an OCI index manifest entry as `"os": "linux"` and `"architecture":"amd64"`.
4. The provider artifact must be available at a tag whose name matches the upstream version number. OpenTofu will ignore any versions it cannot identify as a semver version number, including the `latest` tag.
Because semver uses `+` as the delimiter for "build metadata" and that character is not allowed in an OCI tag name, any `+` characters in the version number must be replaced with `_` when naming the tag.
@ -92,7 +96,7 @@ Currently, there is no third-party tool capable of pushing an OCI artifact in th
Therefore if the ORAS multi-platform manifest proposal does not reach implementation and release before we complete implementation of provider installation from OCI mirrors then we will start by publishing instructions on how to manually write a multi-platform index manifest and push it with the lower-level ORAS manifest commands. The instructions we publish will produce the same effect as the `oras manifest index create` command proposed in [the ORAS Multi-arch Image Management proposal](https://github.com/oras-project/oras/blob/fb6e94d00e59ea6d468cbf8656cf760ef7f1751c/docs/proposals/multi-arch-image-mgmt.md).
We are considering offering a built-in tool for automatically mirroring a set of providers from their origin registries into an OCI mirror, similar to the current [`tofu providers mirror`](https://opentofu.org/docs/cli/commands/providers/mirror/) automating the population of a _filesystem_ mirror directory. However, we wish to minimize the scope for the initial release to maximize our ability to respond to feedback without making breaking changes.
We are considering offering a built-in tool for automatically mirroring a set of providers from their origin registries into an OCI mirror, similar to the current [`tofu providers mirror`](https://opentofu.org/docs/cli/commands/providers/mirror/) automating the population of a _filesystem_ mirror directory. However, we wish to minimize the scope for the initial release to maximize our ability to respond to feedback without making breaking changes, and so we are deferring that for later.
---

View File

@ -9,7 +9,7 @@ This document is part of the [OCI registries RFC](../20241206-oci-registries.md)
---
In contrast to [providers](4-providers.md), modules already support schemes as part of their addresses. Therefore, modules will be addressable from OCI using the `oci://` prefix in OpenTofu code directly. For example, you will be able to address an AWS ECR registry like this:
In contrast to [providers](4-providers.md), modules already support various different schemes as part of their source addresses. Therefore, modules will be addressable from OCI using the `oci://` prefix in OpenTofu code directly. For example, an author would be able to refer to a repository in OCI registry `example.com` like this:
```hcl
module "foo" {
@ -21,7 +21,7 @@ OpenTofu will attempt to find a tag named `latest` for the `opentofu-vpu-module`
## Explicit tag or digest selection
Following the established precedent for [OpenTofu remote module source addresses](https://opentofu.org/docs/language/modules/sources/), the `oci:` scheme will support two optional query string arguments, which are mutually-exclusive:
Following the established precedent for [OpenTofu remote module source addresses](https://opentofu.org/docs/language/modules/sources/), the `oci:` scheme will support two optional and mutually-exclusive arguments using URL query string syntax:
* `tag=NAME` specifies a different tag to use, overriding the default of `latest`.
* `digest=DIGEST` directly specifies the digest of the image manifest to select, bypassing the tag namespace altogether.
@ -32,6 +32,8 @@ For example, `oci://example.com/opentofu-vpc-module?tag=1.0.0` selects the artif
> Using query string arguments is inconsistent with the typical patterns used in OCI-specific software, which instead uses more concise `@DIGEST` or `:TAG` suffixes.
>
> In the implementation details, OpenTofu module installation is delegated to a third-party library called "go-getter" which has its own opinions about address syntax, and it's from that library that the existing query string convention emerged. Following this approach makes OpenTofu consistent with itself, but at the expense of being slightly inconsistent with other OCI-based software. Trying to follow the OCI conventions closely would go against the grain of go-getter's assumptions -- potentially causing challenges as the system evolves in future -- and would make it harder for OpenTofu users to transfer their knowledge from one source address type to another.
>
> There is additional commentary on this discusion under [Modules in package sub-directories](#modules-in-package-sub-directories), below.
## Version-constraint-based selection
@ -41,11 +43,20 @@ Therefore our first release of these features will rely on direct selection of s
A future version of OpenTofu might support [version constraints for other source address types](https://github.com/opentofu/opentofu/issues/2495) -- potentially including OCI, Git, and Mercurial -- but that is a broader change to OpenTofu's architecture and outside the scope of this RFC.
> [!NOTE]
> The absense of version-constraint-based selection here is also a notable difference from the functionality we're offering for provider mirrors.
>
> This difference extends the existing precedent that the namespace of providers and their versions is intentionally abstracted to allow multiple installation strategies behind the same syntax configured globally as a concern of the environment where OpenTofu is running, whereas module sources are typically specified directly as physical addresses of specific artifacts unless someone chooses to use a module registry to "virtualize" those physical addresses.
>
> Directly exposing the concepts from the underlying protocol -- tags and digests in this case -- is consistent with OpenTofu's existing design precedent for module sources, such as the ability to select arbitrary Git branches, tags, and commits without any additional abstraction. It will be possible for a registry implementing the OpenTofu Module Registry Protocol to return an `oci:`-schemed source address as the physical location of a module package, although we don't expect that to be a common choice at least for the near future since we understand that the primary motivation for installing modules from OCI registries is to avoid running any additional OpenTofu-specific services. (For more commentary on the interaction between OpenTofu module registries and the new source address syntax, refer to [OCI-based Modules through an OpenTofu Module Registry](#oci-based-modules-through-an-opentofu-module-registry) below.)
>
> We'll use the issue linked in the text above to track interest in introducing the additional abstraction of semver-based selection of non-registry sources as an orthogonal concern in a later release, since that will allow us to approach it holistically as something we offer for all source address types whose underlying protocol has some concept we can use to represent "versions", rather than implementing something OCI-registry-specific.
## Modules in package sub-directories
Although we commonly just casually refer to installing "modules", the unit of module installation is technically the module _package_, which is the term we use for an artifact containing a filesystem tree that has zero or more module directories inside of it. For example, a Git repository _as a whole_ is a module package because `git clone` only supports cloning the entirety the filesystem associated with a particular commit, but a Git repository can potentially contain multiple modules.
Although we commonly just casually refer to installing "modules", the unit of module installation is technically the module _package_, which is the term we use for an artifact containing a filesystem tree that has zero or more module directories inside of it. For example, a Git repository _as a whole_ is a module package because `git clone` only supports transferring the entirety of the filesystem tree associated with a particular commit, but a Git repository can potentially contain multiple modules.
An OCI artifact will represent a module package, as described in the following section. As usual OpenTofu expects to find a module in the root directory of an OCI artifact by default, but you can optionally specify a subdirectory using [the usual subdirectory syntax](https://opentofu.org/docs/language/modules/sources/#modules-in-package-sub-directories):
An OCI artifact will represent a module package as described in the following section. As usual, OpenTofu expects to find a module in the root directory of an OCI artifact by default, but an author can optionally specify a subdirectory using [the usual subdirectory syntax](https://opentofu.org/docs/language/modules/sources/#modules-in-package-sub-directories):
```hcl
module "foo" {
@ -53,30 +64,41 @@ module "foo" {
}
```
To combine that with the `tag` or `digest` arguments, place the query string after the subdirectory portion: `oci://example.com/opentofu-vpc-module//subnets?tag=1.0.0`.
To combine that with the `tag` or `digest` arguments, authors must place the query string after the subdirectory portion: `oci://example.com/opentofu-vpc-module//subnets?tag=1.0.0`.
> [!NOTE]
>
> During the design of this we considered having the source addresses use a more "OCI-native" syntax that uses individual punctuation characters rather than URL query string syntax:
>
> - `oci://example.com/opentofu-vpc-module:TAG-NAME`
> - `oci://example.com/opentofu-vpc-module@DIGEST`
>
> We decided to follow OpenTofu's existing precedent in part because although go-getter would accept both of the above as valid URLs, it would not actually understand the meaning of those `:TAG-NAME` and `@DIGEST` suffixes, and so would treat them as part of the path instead. That would then make them interact with the sub-path syntax differently than every other source address syntax -- the sub-path would need to appear _after_ the tag/digest part, rather than before the query string -- making it harder for authors to transfer what they've learned from one source type to use of other source types.
>
> Making OpenTofu consistent with itself rather than with the typical OCI conventions is a tricky tradeoff, but overall following the existing precedent should make it less likely that any future mechanisms we design that are supposed to work across all source address types will need a special/unique design for the `oci:` scheme, rather than a shared design across all of the source types.
## Artifact layout
Module packages will be represented as a direct ORAS-style image artifact, without a multi-platform image manifest, because module packages are platform-agnostic. The `artifactType` property of the image manifest must be set to `application/vnd.opentofu.module`.
Module packages will be represented as a direct ORAS-style image artifact, without a multi-platform image manifest, because module packages are platform-agnostic. The `artifactType` property of the image manifest must be set to `application/vnd.opentofu.modulepkg`.
The image manifest _must_ have exactly one layer with `mediaType` set to `archive/zip`, which refers to a blob containing a `.zip` archive of the module package contents.
The manifest may include other layers with different `mediaType` values, which OpenTofu will ignore. Future versions of OpenTofu might recognize layers with other `mediaType` values.
If needed, you can publish additional artifacts that refer to the image manifest using the `subject` property in the additional artifact's manifest, making the child artifact discoverable using [the OCI Distribution "referers" API](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers). This is commonly used to attach signatures or metadata such as SBOM documents to an artifact without needing to directly modify the original artifact. OpenTofu will not initially make any use of referring artifacts, but may begin to make use of referrers with specific `artifactType` values in future versions.
If needed, operators can publish additional artifacts that refer to the image manifest using the `subject` property in the additional artifact's manifest, making the child artifact discoverable using [the OCI Distribution "referers" API](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers). This is commonly used to attach signatures or metadata such as SBOM documents to an artifact without needing to directly modify the original artifact. OpenTofu will not initially make any use of referring artifacts, but may begin to make use of referrers with specific `artifactType` values in future versions.
## Tooling
## Publishing module packages
The artifact layout described is compatible with [ORAS](https://oras.land/), so you can use ORAS to push modules:
The artifact layout described is compatible with [ORAS](https://oras.land/), so operators can use ORAS CLI to push modules:
```
oras push \
--artifact-type application/vnd.opentofu.module \
--artifact-type application/vnd.opentofu.modulepkg \
example.com/opentofu-vpc-module:latest \
opentofu-vpc-module.zip:archive/zip
```
## OCI-based Modules through an OpenTofu Module Registry
## OCI-based modules through an OpenTofu Module Registry
The existing [OpenTofu Module Registry Protocol](https://opentofu.org/docs/internals/module-registry-protocol/) works as a façade over arbitrary remote [module source addresses](https://opentofu.org/docs/language/modules/sources/), and so any OpenTofu version that supports OCI-based module installation will also support module registries that respond to [Download Source Code for a Specific Module Version](https://opentofu.org/docs/internals/module-registry-protocol/#download-source-code-for-a-specific-module-version) with a location property using the `oci://` prefix as described above.

View File

@ -56,11 +56,11 @@ oci_default_credentials {
}
```
Future versions of OpenTofu might support other kinds of "ambient" credentials. Each additional credential discovery method should have its own setting in the `oci_default_credentials` block that allows it to be disabled. If a future version of OpenTofu supports an additional discovery method that an operator wishes to use _instead of_ the Docker-style config files then that operator can disable the Docker-style method by setting `docker_style_config_files` to an empty list, and then configure their chosen alternative method as necessary.
Future versions of OpenTofu might support other kinds of "ambient" credentials. Each additional credential discovery method must have its own setting in the `oci_default_credentials` block that allows it to be individually disabled. If a future version of OpenTofu supports an additional discovery method that an operator wishes to use _instead of_ the Docker-style config files then that operator can disable the Docker-style method by setting `docker_style_config_files` to an empty list, and then configure their chosen alternative method as necessary.
## OpenTofu-specific Explicit Configuration
For those who are using OCI registries _only_ with OpenTofu, or who prefer to separate the credentials used for OpenTofu from those used by other tools, the OpenTofu CLI Configuration language will support a new block type `oci_credentials` which specifies the credentials to use for all OCI Distribution repositories matching a prefix given in the block label.
For those who are using OCI registries _only_ with OpenTofu, or who prefer to separate the credentials used for OpenTofu from those used by other tools, the OpenTofu CLI Configuration language will support a new block type `oci_credentials` which specifies the credentials to use for all OCI Distribution repositories matching a prefix given in the block label:
```hcl
oci_credentials "example.com/foo/bar" {
@ -71,11 +71,11 @@ oci_credentials "example.com/foo/bar" {
}
```
Each `oci_credentials` block _must_ set all of the arguments in one of the following sets:
Each `oci_credentials` block _must_ set all of the arguments in exactly one of the following mutually-exclusive groups:
- `username` and `password`: for registries that use "basic-auth-style" credentials
- `access_token` and `refresh_token`: for registries that use OAuth-style credentials
- `docker_credentials_helper`: provides username and password _indirectly_ using a [Docker-style credential helper](https://github.com/docker/docker-credential-helpers)
- `docker_credentials_helper` alone: provides username and password _indirectly_ using a [Docker-style credential helper](https://github.com/docker/docker-credential-helpers)
> [!NOTE]
> Using a separate top-level block for each repository, rather than grouping all of these settings together under a single block, follows the established precedent for OpenTofu-native service authentication configuration.
@ -94,7 +94,7 @@ OpenTofu will then select the most specific match where, for example:
* `example.com/foo` is more specific than just `example.com`.
* Anything involving a matching domain is more specific than a global setting (and only the default Docker-style credentials helper is a "global" setting).
If there is both an explicit credentials configuration and an "ambient" credentials configuration with the same repository address then the explicit `oci_credentials` block takes precedence. The CLI Configuration parser will reject any CLI configuration that includes multiple `oci_credentials` blocks for the same repository prefix. There is no such constraint on the "ambient" credentials, and so OpenTofu will prefer credentials from files earlier in the discovery sequence, or earlier in the `docker_style_config_files` setting.
If there is both an explicit credentials configuration and an "ambient" credentials configuration with the same repository address then the explicit `oci_credentials` block takes precedence. The CLI Configuration parser will reject any CLI configuration that includes multiple `oci_credentials` blocks for the same repository prefix. There is no such constraint on the "ambient" credentials, and so OpenTofu will prefer credentials from files earlier in the automatic discovery sequence, or earlier in the `docker_style_config_files` setting.
---

View File

@ -30,7 +30,7 @@ If we learn that this is a common situation then we may wish to introduce suppor
OpenTofu's provider source address syntax allows a wide variety of Unicode characters in all three components, following the [RFC 3491 "Nameprep"](https://datatracker.ietf.org/doc/rfc3491/) rules.
However, the OCI Distribution specification has a considerably more restrictive allowed character set for repository names: it supports only ASCII letters and digits along with a small set of punctuation.
However, the OCI Distribution specification has a considerably more restrictive allowed character set for repository names: it supports only ASCII letters and digits along with a small set of punctuation characters.
Because of this, there some valid OpenTofu provider source addresses that cannot be translated mechanically to valid OCI Distribution repository addresses via template substitution alone. A provider source address that, for example, has a Japanese alphabet character in its "type" portion would be projected into a syntactically-invalid OCI repository address.
@ -47,7 +47,7 @@ Of course, we cannot see into every organization to know whether they have in-ho
If we learn in future that supporting non-ASCII characters in provider source addresses installed from OCI registries is important, we could potentially force a specific scheme for automatically transforming those names into ones that are compatible with the OCI repository name requirements, such as applying a "[Punycode](https://en.wikipedia.org/wiki/Punycode)-like" encoding to them before rendering them into the template.
However, Punycode in particular is not generally human-readable and so translation strategies like this often require some UI support to automatically transcode the data back into human-readable form for display. Any OpenTofu-specific mapping strategy we might invent is unlikely to be handled automatically by the UI associated with any general-purpose OCI registry implementation.
However, Punycode in particular is not human-readable and so translation strategies like this often require some UI support to automatically transcode the data back into human-readable form for display. Any OpenTofu-specific mapping strategy we might invent is unlikely to be handled automatically by the UI associated with any general-purpose OCI registry implementation.
---

View File

@ -18,16 +18,21 @@ Therefore our primary concern for implementation is in centrally-managing the cr
## OpenTofu CLI Configuration and `package main`
Although there is considerable existing legacy code not following this pattern, the current intended design for OpenTofu is to follow the [dependency inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) with `package main` acting as the ultimate arbiter of how different subsystems are configured to work together. The main package in turn uses `package cliconfig` (`internal/command/cliconfig`) to decide most of the locally-user-configurable settings that can affect those dependency resolution decisions.
Although there is considerable existing legacy code not following this pattern, the current intended design for OpenTofu is to follow [the dependency inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) with `package main` acting as the ultimate arbiter of how different subsystems are configured to work together. The main package in turn uses `package cliconfig` (`internal/command/cliconfig`) to decide most of the locally-user-configurable settings that can affect those dependency resolution decisions.
We will continue that design approach by teaching `package cliconfig` to decode and validate the `oci_default_credentials` and `oci_credentials` blocks in the CLI configuration, returning the discovered information as part of the overall `cliconfig.Config` object generated by that package.
The implicit configuration mode acts as an alternative way to populate the same settings from Docker CLI or other container system configuration files, and so would also be implemented in `package cliconfig` by mapping the concepts from the Docker CLI configuration language to the same internal data types that we would decode our explicit configuration into, so that the rest of the system does not need to be concerned about how that information was discovered.
The implicit configuration mode acts as an alternative way to populate the same settings from Docker CLI or other container system configuration files, and so would also be accessed through `package cliconfig` by mapping the concepts from the Docker CLI configuration language to the same internal data types that we would decode our explicit configuration into, so that the rest of the system does not need to be concerned about how that information was discovered.
`package main` is responsible for using the information returned from the CLI configuration to configure and instantiate the [`ociclient.OCIClient`](https://pkg.go.dev/github.com/opentofu/libregistry@v0.0.0-20241121135917-6f06a9a60bb5/registryprotocols/ociclient#OCIClient) that will then be passed as a dependency into both the provider installer and the module installer, which will then encapsulate all of the OCI registry interactions including the collection and inclusion of credentials when making requests.
`package main` is responsible for using the information returned from the CLI configuration to tell other subsystems how to configure and instantiate an OCI Distribution client, which will then encapsulate all of the OCI registry interactions, including the selection of and inclusion of appropriate credentials when making requests.
## OCI Registry Credentials Policy Layer
> [!NOTE]
> The Go signatures included below are intended to be illustrative of overall architecture rather than concrete/final. Although the final implementation will follow the same general structure described here, the specific methods, arguments, etc will be finalized during the implementation phase based on the discovered practical requirements, and thus will be discussed primarily during code review rather than RFC review.
>
> If you are reading this document after the implementation is complete, prefer to consult the real API documentation in the current Go packages in case the finer details have changed from what's described here.
Since the mechanisms for configuring credentials are non-trivial and the logic for selecting a single set of credentials based on the configuration are relatively complex, we will encapsulate the model of credentials-selection policy into a separate `package ociauthconfig`, placed at `internal/command/cliconfig/ociauthconfig` to reflect its close relationship with the CLI configuration language.
The main functionality in this package will be a function that takes a set of objects representing individual sources of credentials configuration (e.g. individal Docker CLI configuration files, or blocks from the main OpenTofu CLI Configuration), finds all of the credential sources relevant to a particular OCI repository address, and then chooses the one that matches the repository address most specifically as described in [Credentials Selection Precedence](6-authentication.md#credentials-selection-precedence).
@ -61,11 +66,11 @@ This in turn returns a sequence of objects implementing another interface `Crede
```go
type CredentialsSource interface {
CredentialsSpecificity() CredentialsSpecificity
Credentials(ctx context.Context, env CredentialsLookupEnvironment) (Credentials, error)
Credentials(ctx context.Context) (Credentials, error)
}
```
This additional indirection allows the main credentials-selection logic to first use the "specificity" value to choose a single credentials source to use, and only then to call `Credentials` on it to obtain the final concrete credentials to use. This is particularly important for the `CredentialsSource` that wrap Docker-style credentials helpers, since we'll want to avoid executing any credentials helper unless it's the finally-chosen single source. (`CredentialsLookupEnvironment` is a dependency-inversion adapter allowing the implementation to interact with external concerns like executing child processes, to allow mock implementations for easier testing.)
This additional indirection allows the main credentials-selection logic to first use the "specificity" value to choose a single credentials source to use, and only then to call `Credentials` on it to obtain the final concrete credentials to use. This is particularly important for the `CredentialsSource` implementation that wraps Docker-style credentials helpers, since we'll want to avoid executing any credentials helper until it's the finally-chosen single source.
`Credentials` is an opaque struct type encapsulating some concrete credentials. Since our initial implementation will rely on the OCI Distribution client implementation from the upstream library from the ORAS project, `Credentials` will initially offer just a single method for translating our internal representation into the representation expected by that library, with the expectation that we'll change this specific API later if we find a reason to use a different client library:
@ -119,10 +124,10 @@ func (cc *CredentialsConfigs) CredentialsSourceForRepository(ctx context.Context
The pseudocode for the implementation of this function is:
- Let `result` be a `nil` value of type `CredentialsSource`, and `resultSpec` be initialized as `NoCredentialsSpecificity`.
- For each `CredentialsConfig` implementation, `cfg`:
- For each `CredentialsConfig` implementation, named `cfg`:
- Let `sources` be the result of `cfg.CredentialsSourcesForRepository` with the given `registryDomain` and `repositoryPath`.
- For each `CredentialsSource` implementation in `sources`, `source`:
- If `source.CredentialsSpecificity` (`candidateSpec`) is greater than `resultSpec`:
- For each `CredentialsSource` implementation in `sources`, named `source`:
- If `source.CredentialsSpecificity` (named `candidateSpec`) is greater than `resultSpec`:
- Assign `source` to `result` and `candidateSpec` to `resultSpec`.
- If `resultSpec` is still `NoCredentialsSpecificity`, return a "no credentials available" error and terminate.
- Otherwise, return `result` with no error and terminate.
@ -136,7 +141,7 @@ Once `package cliconfig` has constructed a `CredentialsConfigs` object based on
> [!NOTE]
> Ideally we would rely on a third-party library for this non-OpenTofu-specific concern, but in our review of some candidates we found that they are typically not extensible to allow us to integrate our own OpenTofu CLI Configuration-based explicit configuration method. Many of them also come only as part of larger libraries with a significant number of indirect dependencies that we would not otherwise need, and would likely cause false positives for naive security scanners that work only at a whole-Go-module granularity.
>
> Therefore we'll use our own implementation of this concern at least for the first round, but since all of this functionality will be in OpenTofu's `internal` packages for the foreseeable future we will have the freedom to swap for an upstream implementation of similar functionality later if we become aware of one.
> Therefore we'll use our own implementation of this concern at least for the first round, but since all of this functionality will be in OpenTofu's `internal` packages for the foreseeable future we will have the freedom to swap for an upstream implementation of similar functionality later if we become aware of one, as long as it is sufficiently compatible. The matching rules above are based on those documented for the Docker-style configuration language, and so we can reasonably assume that third-party libraries would match it well enough.
## OCI Client for the Provider Installer

View File

@ -40,6 +40,9 @@ The registry client type can then in turn produce [a repository-specific client]
We expect that the implementation of `OCIMirrorSource` will be very similar to the existing `HTTPMirrorSource`, but will of course use the OCI Distribution protocol when making requests instead of OpenTofu's own "network mirror protocol".
> [!NOTE]
> The above signature for `NewOCIMirrorSource` is intended to be illustrative rather than concrete. In practice it may end up taking a single argument that is an implementation of some interface with two methods similar to the function arguments shown, but that's a level of design detail we will consider during the implementation phase, with consideration to whether it turns out to be shareable with the similar dependency inversion design we'll be using for the module installer.
## Package checksums and signing
OpenTofu tracks in its [dependency lock file](https://opentofu.org/docs/language/files/dependency-lock/) the single selected version of each previously-installed provider, and a set of checksums that are considered acceptable for that version of that provider.
@ -50,9 +53,9 @@ OpenTofu's existing provider registry protocol always uses `.zip` archives as pr
[Our OCI artifact layout for provider packages](4-providers.md#storage-in-oci) intentionally uses `archive/zip` layers so that we can use byte-for-byte identical copies of a provider developer's signed `.zip` packages as the layer blobs, and therefore we can directly translate the `sha256:` digest of each layer into a `zh:`-style checksum for dependency locking purposes, without having to download the package and recalculate the checksum locally.
We are not intending to support OCI artifact signing in our first implementation, since we are focusing initially only on the "OCI mirror" use-case. Without any signatures, we'll capture in the dependency lock file only the checksum of the specific artifact we downloaded, for consistency with the guarantees we make from installing from unsigned sources in today's OpenTofu. When we later add support for optionally signing the index manifest, we can begin using those signatures to justify including the `zh:` checksums from _all_ of the per-platform artifacts, announcing the ID of the signing key as part of the `tofu init` output just as we do for registry-distributed signatures today. It remains the user's responsibility to verify that the key ID is one they expected before committing the new checksums in the dependency lock file.
We are not intending to support OCI artifact signing in our first implementation, since we are focusing initially only on the "OCI mirror" use-case where the mirror is assumed to be trusted by whoever configured it. Without any signatures, we'll capture in the dependency lock file only the checksums (both `h1:` and `zh:`) of the specific artifact we downloaded, for consistency with the guarantees we make from installing from unsigned sources in today's OpenTofu. When we later add support for optionally signing the index manifest, we can begin using those signatures to justify including the `zh:` checksums from _all_ of the per-platform artifacts, announcing the ID of the signing key as part of the `tofu init` output just as we do for registry-distributed signatures today. It would remain the operator's responsibility to verify that the key ID is one they expected before committing the new checksums in the dependency lock file.
As with OpenTofu's current provider registry protocol, an OCI provider artifact cannot provide us any trustworthy representation of the `h1:` checksum of a provider package, and so OpenTofu will calculate that locally based on the already-downloaded package(s), assuming that they also match one of the previously-discovered `zh:` checksums, as usual. Operators will be able to use the existing `tofu providers lock` command to force OpenTofu to calculate and record `h1:` checksums for other platforms if desired, just as is possible today with all of the existing provider installation methods.
As with OpenTofu's current provider registry protocol, an OCI provider artifact manifest cannot provide us any trustworthy representation of the `h1:` checksum of a provider package, and so OpenTofu will calculate that locally based on the already-downloaded package(s) only if the artifact also matches one of the previously-discovered `zh:` checksums, as usual. Operators will be able to use the existing `tofu providers lock` command to force OpenTofu to calculate and record `h1:` checksums for other platforms if desired, just as is possible today with all of the existing provider installation methods.
> [!NOTE]
> The `tofu providers lock` command by default ignores the operator's configured provider installation methods and instead always attempts to install providers from their origin registries. This default is to support the workflow where `tofu providers lock` is used to obtain the checksums of the "official" releases from the origin registry and then subsequent `tofu init` against a mirror source can verify that the mirrored packages match those official release packages without needing to directly access the origin registry.