Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Git commit and tag signature verification
Browse files Browse the repository at this point in the history
This feature adds the `--git-verify-signatures` flag to the daemon.
When this flag is set the daemon will verify the signatures of the tag
and all commits it is working with, ensuring no unauthorized
modifications are synchronized.

To ensure the daemon always synchronizes a verified state a ratchet
mechanism was introduced to the loop. This mechanism moves the sync
HEAD to the latest valid revision after repository refreshes. During
sync runs this revision gets checked out and is applied on to the
cluster.

During modification actions, either automated or instructed by fluxctl
commands, the HEAD of the working clone is compared to the latest valid
revision. If these mismatch the commit is blocked and an error is
returned as the daemon can not be sure it is committing on top of a
verified state.
  • Loading branch information
hiddeco committed Mar 20, 2019
1 parent 8c82f10 commit 4b98233
Show file tree
Hide file tree
Showing 12 changed files with 829 additions and 684 deletions.
27 changes: 15 additions & 12 deletions cmd/fluxd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ func main() {
gitTimeout = fs.Duration("git-timeout", 20*time.Second, "duration after which git operations time out")

// GPG commit signing
gitImportGPG = fs.String("git-gpg-key-import", "", "keys at the path given (either a file or a directory) will be imported for use in signing commits")
gitSigningKey = fs.String("git-signing-key", "", "if set, commits will be signed with this GPG key")
gitImportGPG = fs.String("git-gpg-key-import", "", "keys at the path given (either a file or a directory) will be imported for use in signing commits")
gitSigningKey = fs.String("git-signing-key", "", "if set, commits will be signed with this GPG key")
gitVerifySignatures = fs.Bool("git-verify-signatures", false, "if set, commits will be verified before Flux applies them")

// syncing
syncInterval = fs.Duration("sync-interval", 5*time.Minute, "apply config in git to cluster at least this often, even if there are no new commits")
Expand Down Expand Up @@ -433,15 +434,17 @@ func main() {

gitRemote := git.Remote{URL: *gitURL}
gitConfig := git.Config{
Paths: *gitPath,
Branch: *gitBranch,
SyncTag: *gitSyncTag,
NotesRef: *gitNotesRef,
UserName: *gitUser,
UserEmail: *gitEmail,
SigningKey: *gitSigningKey,
SetAuthor: *gitSetAuthor,
SkipMessage: *gitSkipMessage,
Paths: *gitPath,
Branch: *gitBranch,
SyncTag: *gitSyncTag,
NotesRef: *gitNotesRef,
UserName: *gitUser,
UserEmail: *gitEmail,
SigningKey: *gitSigningKey,
VerifySignatures: *gitVerifySignatures,
SetAuthor: *gitSetAuthor,
SkipMessage: *gitSkipMessage,
Timeout: *gitTimeout,
}

repo := git.NewRepo(gitRemote, git.PollInterval(*gitPollInterval), git.Timeout(*gitTimeout))
Expand All @@ -460,6 +463,7 @@ func main() {
"user", *gitUser,
"email", *gitEmail,
"signing-key", *gitSigningKey,
"verify-signatures", *gitVerifySignatures,
"sync-tag", *gitSyncTag,
"notes-ref", *gitNotesRef,
"set-author", *gitSetAuthor,
Expand All @@ -484,7 +488,6 @@ func main() {
LoopVars: &daemon.LoopVars{
SyncInterval: *syncInterval,
RegistryPollInterval: *registryPollInterval,
GitOpTimeout: *gitTimeout,
},
}

Expand Down
65 changes: 65 additions & 0 deletions daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,11 @@ func (d *Daemon) sync() jobFunc {
if err != nil {
return result, err
}
if latestVerifiedRev, err := d.LatestValidRevision(ctx, head); err != nil {
return result, err
} else if head != latestVerifiedRev {
return result, fmt.Errorf("unable to sync to invalid HEAD revision (%s) latest verified revision is: %s", head, latestVerifiedRev)
}
result.Revision = head
return result, nil
}
Expand Down Expand Up @@ -418,6 +423,14 @@ func (d *Daemon) updatePolicy(spec update.Spec, updates policy.Updates) updateFu
return result, nil
}

if headRev, err := working.HeadRevision(ctx); err != nil {
return result, err
} else if latestVerifiedRev, err := d.LatestValidRevision(ctx, headRev); err != nil {
return result, err
} else if headRev != latestVerifiedRev {
return result, fmt.Errorf("HEAD (%s) is not a verified revision; can not update on top of unverified HEAD", headRev)
}

commitAuthor := ""
if d.GitConfig.SetAuthor {
commitAuthor = spec.Cause.User
Expand Down Expand Up @@ -459,6 +472,14 @@ func (d *Daemon) release(spec update.Spec, c release.Changes) updateFunc {
var revision string

if c.ReleaseKind() == update.ReleaseKindExecute {
if headRev, err := working.HeadRevision(ctx); err != nil {
return zero, err
} else if latestVerifiedRev, err := d.LatestValidRevision(ctx, headRev); err != nil {
return zero, err
} else if headRev != latestVerifiedRev {
return zero, fmt.Errorf("HEAD (%s) is not a verified revision; can not update on top of unverified HEAD", headRev)
}

commitMsg := spec.Cause.Message
if commitMsg == "" {
commitMsg = c.CommitMessage(result)
Expand Down Expand Up @@ -614,6 +635,50 @@ func (d *Daemon) WithClone(ctx context.Context, fn func(*git.Checkout) error) er
return fn(co)
}

// LatestValidRevision returns the latest valid revision for the
// configured branch when the verification of GPG signatures for Git
// is enabled _or_ the HEAD revision of the configured branch when it
// is not. In case verification is enabled and a current revision is
// given it will also validate the tag signature -- as the state of
// the branch can not be trusted when the tag originates from an
// unknown source.
func (d *Daemon) LatestValidRevision(ctx context.Context, currentRevision string) (string, error) {
newRevision, err := d.Repo.Revision(ctx, d.GitConfig.Branch)
if !d.GitConfig.VerifySignatures || err != nil {
return newRevision, err
}

if currentRevision != "" {
err = d.Repo.VerifyTag(ctx, d.GitConfig.SyncTag)
if err != nil {
return currentRevision, errors.Wrap(err, "failed to verify signature of sync tag")
}
}

var commits []git.Commit
if currentRevision == "" {
commits, err = d.Repo.CommitsBefore(ctx, newRevision)
} else {
commits, err = d.Repo.CommitsBetween(ctx, currentRevision, newRevision)
}

if err != nil {
return "", err
}

for i := len(commits) - 1; i >= 0; i-- {
if !commits[i].Signature.Valid() {
d.Logger.Log("err", "invalid GPG signature for commit", "revision", commits[i].Revision, "key", commits[i].Signature.Key)
if i+1 < len(commits) {
return commits[i+1].Revision, nil
}
return "", nil
}
}

return newRevision, nil
}

func (d *Daemon) LogEvent(ev event.Event) error {
if d.EventWriter == nil {
d.Logger.Log("event", ev, "logupstream", "false")
Expand Down
3 changes: 2 additions & 1 deletion daemon/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ func mockDaemon(t *testing.T) (*Daemon, func(), func(), *cluster.Mock, *mockEven
UserEmail: "example@example.com",
SyncTag: "flux-test",
NotesRef: "fluxtest",
Timeout: timeout,
}

var k8s *cluster.Mock
Expand Down Expand Up @@ -742,7 +743,7 @@ func mockDaemon(t *testing.T) (*Daemon, func(), func(), *cluster.Mock, *mockEven
JobStatusCache: &job.StatusCache{Size: 100},
EventWriter: events,
Logger: logger,
LoopVars: &LoopVars{GitOpTimeout: timeout},
LoopVars: &LoopVars{},
}

start := func() {
Expand Down
Loading

0 comments on commit 4b98233

Please sign in to comment.