Skip to content
Merged
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
3 changes: 2 additions & 1 deletion api/v1/gitrepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ type GitRepositoryVerification struct {
Mode GitVerificationMode `json:"mode,omitempty"`

// SecretRef specifies the Secret containing the public keys of trusted Git
// authors.
// authors. PGP public keys must be stored under keys with the .asc suffix,
// and SSH public keys must be stored under keys with the .sshpub suffix.
// +required
SecretRef meta.LocalObjectReference `json:"secretRef"`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ spec:
secretRef:
description: |-
SecretRef specifies the Secret containing the public keys of trusted Git
authors.
authors. PGP public keys must be stored under keys with the .asc suffix,
and SSH public keys must be stored under keys with the .sshpub suffix.
properties:
name:
description: Name of the referent.
Expand Down
3 changes: 2 additions & 1 deletion docs/api/v1/source.md
Original file line number Diff line number Diff line change
Expand Up @@ -2491,7 +2491,8 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</td>
<td>
<p>SecretRef specifies the Secret containing the public keys of trusted Git
authors.</p>
authors. PGP public keys must be stored under keys with the .asc suffix,
and SSH public keys must be stored under keys with the .sshpub suffix.</p>
</td>
</tr>
</tbody>
Expand Down
43 changes: 42 additions & 1 deletion docs/spec/v1/gitrepositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,10 @@ signatures. The field offers two subfields:
the commit object pointed to by the tag.

- `.secretRef.name`, to specify a reference to a Secret in the same namespace as
the GitRepository. Containing the (PGP) public keys of trusted Git authors.
the GitRepository. Containing the public keys of trusted Git authors. PGP
public keys must be stored under keys with the `.asc` suffix, and SSH public
keys must be stored under keys with the `.sshpub` suffix. Keys without a
recognized suffix are treated as PGP key rings for backward compatibility.

```yaml
---
Expand Down Expand Up @@ -695,6 +698,44 @@ kubectl create secret generic pgp-public-keys \
-o yaml
```

#### SSH verification

SSH-signed commits and tags can also be verified. Store SSH public keys in
`authorized_keys` format under keys with the `.sshpub` suffix in the same
Secret:

```yaml
---
apiVersion: v1
kind: Secret
metadata:
name: verification-keys
namespace: default
type: Opaque
data:
author1.asc: <BASE64 PGP public key>
author2.sshpub: <BASE64 SSH public key>
```

Generating an SSH key pair and creating the Secret:

```sh
# Generate an SSH key pair for signing
ssh-keygen -t ed25519 -N '' -f /tmp/signing_key
# Generate secret with the public key
kubectl create secret generic verification-keys \
--from-file=author2.sshpub=/tmp/signing_key.pub \
-o yaml
```

A single Secret can contain both PGP (`.asc`) and SSH (`.sshpub`) keys. The
controller detects the signature type of each Git object (PGP or SSH) and
dispatches verification accordingly.

PGP verification reports the PGP key ID in the success message (e.g.
`5982D0279C227FFD`), while SSH verification reports the SHA256 fingerprint
(e.g. `SHA256:uNiVztksCsDhcc0u9e8BgrJXVGDaf6s7kOsTmI9N7sM`).

### Ignore

`.spec.ignore` is an optional field to specify rules in [the `.gitignore`
Expand Down
65 changes: 52 additions & 13 deletions internal/controller/gitrepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,41 @@ import (
"github.com/fluxcd/source-controller/internal/util"
)

const (
// publicKeyPGPSuffix is the Secret data key suffix for PGP public keys.
publicKeyPGPSuffix = ".asc"
// publicKeySSHSuffix is the Secret data key suffix for SSH public keys.
publicKeySSHSuffix = ".sshpub"
)

// gitSigner abstracts the verification methods shared by git.Commit and git.Tag.
type gitSigner interface {
SignatureType() string
VerifyPGP(keyRings ...string) (string, error)
VerifySSH(authorizedKeys ...string) (string, error)
}

// verifyGitObject dispatches signature verification based on the signature type
// of the given git object. It returns the key identity (PGP key ID or SSH
// fingerprint) on success, or an error if verification fails or the required
// key type is missing from the Secret.
func verifyGitObject(obj gitSigner, keyRings []string, authorizedKeys []string) (string, error) {
switch obj.SignatureType() {
case "openpgp":
if len(keyRings) == 0 {
return "", fmt.Errorf("PGP signature detected but no PGP public keys found in secret (keys with %s suffix)", publicKeyPGPSuffix)
}
return obj.VerifyPGP(keyRings...)
case "ssh":
if len(authorizedKeys) == 0 {
return "", fmt.Errorf("SSH signature detected but no SSH public keys found in secret (keys with %s suffix)", publicKeySSHSuffix)
}
return obj.VerifySSH(authorizedKeys...)
default:
return "", fmt.Errorf("unsupported signature type: %s", obj.SignatureType())
}
}

// gitRepositoryReadyCondition contains the information required to summarize a
// v1.GitRepository Ready Condition.
var gitRepositoryReadyCondition = summarize.Conditions{
Expand Down Expand Up @@ -1093,24 +1128,32 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
return sreconcile.ResultSuccess, nil
}

// Get secret with GPG data
// Get secret with public key data
publicKeySecret := types.NamespacedName{
Namespace: obj.Namespace,
Name: obj.Spec.Verification.SecretRef.Name,
}
secret := &corev1.Secret{}
if err := r.Client.Get(ctx, publicKeySecret, secret); err != nil {
e := serror.NewGeneric(
fmt.Errorf("PGP public keys secret error: %w", err),
fmt.Errorf("public keys secret error: %w", err),
"VerificationError",
)
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, "%s", e)
return sreconcile.ResultEmpty, e
}

var keyRings []string
for _, v := range secret.Data {
keyRings = append(keyRings, string(v))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation does not implement what is stated in the documentation: https://fluxcd.io/flux/components/source/gitrepositories/#verification-secret-example

Any key (not just .asc) is used for PGP public keys. Therefore a fallback is implemented to not break current setups

var authorizedKeys []string
for k, v := range secret.Data {
if strings.HasSuffix(k, publicKeySSHSuffix) {
authorizedKeys = append(authorizedKeys, string(v))
} else if strings.HasSuffix(k, publicKeyPGPSuffix) {
keyRings = append(keyRings, string(v))
} else {
// Provide fallback to support previous undocumented behavior
keyRings = append(keyRings, string(v))
}
}

var message strings.Builder
Expand Down Expand Up @@ -1140,38 +1183,34 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
return sreconcile.ResultEmpty, err
}

// Verify tag with GPG data from secret
tagEntity, err := tag.Verify(keyRings...)
entity, err := verifyGitObject(tag, keyRings, authorizedKeys)
if err != nil {
e := serror.NewGeneric(
fmt.Errorf("signature verification of tag '%s' failed: %w", tag.String(), err),
"InvalidTagSignature",
)
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, "%s", e)
// Return error in the hope the secret changes
return sreconcile.ResultEmpty, e
}

message.WriteString(fmt.Sprintf("verified signature of\n\t- tag '%s' with key '%s'", tag.String(), tagEntity))
message.WriteString(fmt.Sprintf("verified signature of\n\t- tag '%s' with key '%s'", tag.String(), entity))
}

if obj.Spec.Verification.VerifyHEAD() {
// Verify commit with GPG data from secret
headEntity, err := commit.Verify(keyRings...)
entity, err := verifyGitObject(&commit, keyRings, authorizedKeys)
if err != nil {
e := serror.NewGeneric(
fmt.Errorf("signature verification of commit '%s' failed: %w", commit.Hash.String(), err),
"InvalidCommitSignature",
)
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, "%s", e)
// Return error in the hope the secret changes
return sreconcile.ResultEmpty, e
}
// If we also verified the tag previously, then append to the message.
if message.Len() > 0 {
message.WriteString(fmt.Sprintf("\n\t- commit '%s' with key '%s'", commit.Hash.String(), headEntity))
message.WriteString(fmt.Sprintf("\n\t- commit '%s' with key '%s'", commit.Hash.String(), entity))
} else {
message.WriteString(fmt.Sprintf("verified signature of\n\t- commit '%s' with key '%s'", commit.Hash.String(), headEntity))
message.WriteString(fmt.Sprintf("verified signature of\n\t- commit '%s' with key '%s'", commit.Hash.String(), entity))
}
}

Expand Down
Loading
Loading