Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ module/services/telemetry/code/pkg/gen/** linguist-generated=true
# Build recipes emitted by the pinned Go service agent.
module/services/accounts/builder/** linguist-generated=true
module/services/auth-gateway/builder/** linguist-generated=true

# Native protocol and catalog output; authored proto inputs and dependency files stay visible.
/module/services/accounts/code/pkg/gen/** linguist-generated=true
/module/services/accounts/generated/** linguist-generated=true
/module/services/frontend/code/src/gen/** linguist-generated=true
/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/** linguist-generated=true
/module/contracts/api/accounts/authority/proto/host/** linguist-generated=true
/module/contracts/api/accounts/connect/proto/host/** linguist-generated=true
2 changes: 1 addition & 1 deletion libraries/source-read-sdk/library.codefly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ sources:
module: saas-starter
service: accounts
endpoint: authority
contract-digest: sha256:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831
contract-digest: sha256:6eb1f3eb1ba262017ff4bdfafe047cba7b441855affd67f44285804bdda9e4d5
services:
- ModuleCapabilitiesService
generated-by:
Expand Down
21 changes: 20 additions & 1 deletion module/KEY_ROTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,26 @@ TTL — or within TTL + grace if accounts is unreachable for the whole window.
> so there is no projection to read and no agent that owns the driver the way
> `service-vault` owns Vault's. Filed as codefly-dev/cli#924. Everything above
> the seam is already vendor-neutral, so when that kind exists the driver moves
> below it and nothing here changes.
> below it and nothing here changes. New managed providers use the owner runtime
> client described below; the legacy GCP implementation remains unchanged.

`KEY_SERVICE_MANAGED_BINDING` optionally carries the deployment-produced,
non-secret provider descriptor consumed by `service-vault/keyservice`. Accounts
depends on that client's generic encryption, MAC and wrapping operations; no
second cloud SDK or driver lives here. The existing `kms` / `kms-wrapped`
selections still apply. Mixing a descriptor with legacy KMS key names is refused.

The client owns provider ciphertexts, key-version checks and workload identity;
Accounts retains `cfs1` envelope framing, domain errors, tenant-key selection and
the `signing-key-wrap` purpose. The unwrapped value must still be a base64
Ed25519 seed. A provider outage never falls back to ephemeral signing, including
when a local caller otherwise permits a fixture key.

Build or reuse: the existing Host `Sealer`, `KeyFactory`, `Cipher` and wrapped-seed
path already define the required contracts. The owner client's provider driver
fills the missing implementation; no new Host crypto primitive, signing algorithm
or key store is introduced. Infrastructure allocation, exact workload grants,
recovery custody and live deployment remain separate qualification requirements.


Two backends, selected per key family in the `key-service` group
Expand Down
5 changes: 5 additions & 0 deletions module/configurations/local/key-service.env
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,8 @@ KEY_SERVICE_KMS_MAC_KEY=
# in the envelope framing, each by name.
KEY_SERVICE_KMS_SIGNING_WRAP_KEY=
KEY_SERVICE_SIGNING_KEY_WRAPPED=

# Optional provider-owned managed installation. Supplied by the deployment
# owner; mutually exclusive with the legacy KMS key names above. The existing
# kms and kms-wrapped selections retain their custody and signing semantics.
KEY_SERVICE_MANAGED_BINDING=
Binary file modified module/contracts/api/accounts/authority/contract.binpb
Binary file not shown.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified module/contracts/api/accounts/connect/contract.binpb
Binary file not shown.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions module/contracts/api/catalog.codefly.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"kind": "protobuf",
"package": "saas.accounts.v1",
"path": "contracts/api/accounts/authority/contract.binpb",
"digest": "sha256:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831",
"digest": "sha256:6eb1f3eb1ba262017ff4bdfafe047cba7b441855affd67f44285804bdda9e4d5",
"services": [
{
"name": "ModuleCapabilitiesService",
Expand Down Expand Up @@ -66,7 +66,7 @@
"kind": "protobuf",
"package": "",
"path": "contracts/api/accounts/connect/contract.binpb",
"digest": "sha256:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831",
"digest": "sha256:6eb1f3eb1ba262017ff4bdfafe047cba7b441855affd67f44285804bdda9e4d5",
"services": [
{
"name": "ArtifactService",
Expand Down Expand Up @@ -109,6 +109,7 @@
"/host.installation.v1.InstallationService/GrantRole",
"/host.installation.v1.InstallationService/Install",
"/host.installation.v1.InstallationService/List",
"/host.installation.v1.InstallationService/ListHeld",
"/host.installation.v1.InstallationService/ListRoleGrants",
"/host.installation.v1.InstallationService/Review",
"/host.installation.v1.InstallationService/RevokeRole"
Expand Down
5 changes: 5 additions & 0 deletions module/frontendpayload/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ repository's own host (Artifact Registry answers every blob read that way);
redirects elsewhere and foreign authentication realms are refused. HTTPS is explicit; plain HTTP requires the mesh Service assertion
or a loopback development source. No registry is selected by default.

The Azure SDK reader also follows ACR's dedicated data-download redirect to
`<same-registry>.<region>.data.azurecr.io` over HTTPS. Only credential-free GETs
reach that endpoint; authentication and token exchanges remain on the registry
login host. The cell must admit that registry's regional data endpoint as well.

`DirectorySource` reads Codefly's existing native artifact cache, whose files are
named by the SHA-256 hex digest. It never writes a cache and is refused when the
host reports that it runs in a Pod. Neither adapter is an artifact store:
Expand Down
111 changes: 111 additions & 0 deletions module/frontendpayload/azure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package frontendpayload

import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry"
"github.com/codefly-dev/core/solutionhost"
"oras.land/oras-go/v2/registry"
)

// The same workload-reader stopgap as workload.go, using Azure's credential,
// token exchange and renewal implementation rather than another auth flow.
type azureWorkloadSource struct {
client *azcontainerregistry.BlobClient
repository string
}

func newAzureWorkloadSource(repository string) (Source, error) {
ref, err := registry.ParseReference(repository)
if err != nil || ref.Reference != "" || ref.Repository == "" {
return nil, errors.New("frontend artifact repository must name one repository without a tag or digest")
}
credential, err := azidentity.NewWorkloadIdentityCredential(nil)
if err != nil {
return nil, &readerIdentityError{"Azure frontend artifact reads require the pod's bound workload identity"}
}
return azureRegistrySource(ref, credential, http.DefaultTransport)
}

func azureRegistrySource(ref registry.Reference, credential azcore.TokenCredential, base http.RoundTripper) (Source, error) {
client, err := azcontainerregistry.NewBlobClient("https://"+ref.Registry, credential,
&azcontainerregistry.BlobClientOptions{ClientOptions: azcore.ClientOptions{
Transport: &http.Client{Timeout: 30 * time.Second,
Transport: azureRegistryTransport{registry: ref.Registry, base: base},
CheckRedirect: func(next *http.Request, via []*http.Request) error {
if len(via) > 3 || next.URL.Scheme != "https" || next.URL.User != nil ||
(next.URL.Host != ref.Registry && !azureDataEndpoint(ref.Registry, next.URL.Host)) {
return errRedirectRefused
}
if next.URL.Host != ref.Registry {
next.Header.Del("Authorization")
}
return nil
}},
}})
if err != nil {
return nil, &readerIdentityError{"could not configure the Azure frontend artifact reader"}
}
return &azureWorkloadSource{client: client, repository: ref.Repository}, nil
}

// Private-link ACR downloads redirect to this registry's regional data host.
// Neither arbitrary Azure storage hosts nor another registry are admitted.
func azureDataEndpoint(registry, host string) bool {
name, ok := strings.CutSuffix(registry, ".azurecr.io")
if !ok || strings.ContainsAny(name, ".:") {
return false
}
parts := strings.Split(host, ".")
if len(parts) != 5 || parts[0] != name || parts[2] != "data" || parts[3] != "azurecr" || parts[4] != "io" || parts[1] == "" {
return false
}
for _, ch := range parts[1] {
if !(ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9') {
return false
}
}
return true
}

type azureRegistryTransport struct {
registry string
base http.RoundTripper
}

func (t azureRegistryTransport) RoundTrip(r *http.Request) (*http.Response, error) {
if r.URL.Scheme != "https" || r.URL.User != nil {
return nil, errRedirectRefused
}
if r.URL.Host != t.registry && (!azureDataEndpoint(t.registry, r.URL.Host) || r.Method != http.MethodGet || r.Header.Get("Authorization") != "") {
return nil, errRedirectRefused
}
return t.base.RoundTrip(r)
}

func (s *azureWorkloadSource) Open(ctx context.Context, digest solutionhost.PayloadDigest) (io.ReadCloser, error) {
response, err := s.client.GetBlob(ctx, s.repository, string(digest), nil)
if err != nil {
var status *azcore.ResponseError
if errors.As(err, &status) {
return nil, fmt.Errorf("%w: the registry answered HTTP %d", ErrUnavailable, status.StatusCode)
}
return nil, unavailable(err)
}
// A redirected storage response may omit the registry-specific digest header.
// Prepare verifies the downloaded bytes against the signed digest before serving.
if (response.DockerContentDigest != nil && *response.DockerContentDigest != string(digest)) ||
response.ContentLength == nil || *response.ContentLength < 0 || *response.ContentLength > MaxBundleBytes {
response.BlobData.Close()
return nil, errors.New("frontend artifact descriptor differs from the admitted bundle")
}
return response.BlobData, nil
}
Loading
Loading