diff --git a/.github/workflows/reusable_monitoring.yml b/.github/workflows/reusable_monitoring.yml index d4dbcf3d..ab491c96 100644 --- a/.github/workflows/reusable_monitoring.yml +++ b/.github/workflows/reusable_monitoring.yml @@ -30,8 +30,8 @@ on: required: false type: number default: 14 - identities: - description: 'multiline yaml of certificate subjects and issuers, key subjects, and fingerprints. For certificates, if no issuers are specified, match any OIDC provider' + config: + description: 'multiline yaml of workflow configuration settings, including rekor server URL, identities including certificate subjects and issuers, key subjects, and fingerprints. For certificates, if no issuers are specified, match any OIDC provider' required: false type: string @@ -76,7 +76,7 @@ jobs: run: cat ${{ env.LOG_FILE }} # Skip on first run continue-on-error: true - - run: go run ./cmd/verifier --file ${{ env.LOG_FILE }} --once --monitored-values "${{ inputs.identities }}" --user-agent "${{ format('{0}/{1}/{2}', needs.detect-workflow.outputs.repository, needs.detect-workflow.outputs.ref, github.run_id) }}" + - run: go run ./cmd/consistency --file ${{ env.LOG_FILE }} --once --config-string "${{ inputs.config }}" --user-agent "${{ format('{0}/{1}/{2}', needs.detect-workflow.outputs.repository, needs.detect-workflow.outputs.ref, github.run_id) }}" - name: Upload checkpoint uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: diff --git a/README.md b/README.md index de0e8ec3..751b63b9 100644 --- a/README.md +++ b/README.md @@ -66,26 +66,27 @@ jobs: with: file_issue: true # Strongly recommended: Files an issue on monitoring failure artifact_retention_days: 14 # Optional, default is 14: Must be longer than the cron job frequency - identities: | - certIdentities: - - certSubject: user@domain\.com - - certSubject: otheruser@domain\.com - issuers: - - https://accounts\.google\.com - - https://github\.com/login - - certSubject: https://github\.com/actions/starter-workflows/blob/main/\.github/workflows/lint\.yaml@.* - issuers: - - https://token\.actions\.githubusercontent\.com - subjects: - - subject@domain\.com - fingerprints: - - A0B1C2D3E4F5 - fulcioExtensions: - build-config-uri: - - https://example.com/owner/repository/build-config.yml - customExtensions: - - objectIdentifier: 1.3.6.1.4.1.57264.1.9 - extensionValues: https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.4.0 + config: | + monitoredValues: | + certIdentities: + - certSubject: user@domain\.com + - certSubject: otheruser@domain\.com + issuers: + - https://accounts\.google\.com + - https://github\.com/login + - certSubject: https://github\.com/actions/starter-workflows/blob/main/\.github/workflows/lint\.yaml@.* + issuers: + - https://token\.actions\.githubusercontent\.com + subjects: + - subject@domain\.com + fingerprints: + - A0B1C2D3E4F5 + fulcioExtensions: + build-config-uri: + - https://example.com/owner/repository/build-config.yml + customExtensions: + - objectIdentifier: 1.3.6.1.4.1.57264.1.9 + extensionValues: https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.4.0 ``` In this example, the monitor will log: diff --git a/cmd/rekor_consistency/config.go b/cmd/rekor_consistency/config.go new file mode 100644 index 00000000..18cb0e27 --- /dev/null +++ b/cmd/rekor_consistency/config.go @@ -0,0 +1,29 @@ +// +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "time" + + "github.com/sigstore/rekor-monitor/pkg/identity" +) + +type ConsistencyCheckConfiguration struct { + ServerURL string `yaml:"serverURL"` + Interval *time.Duration `yaml:"interval"` + OutputIdentitiesFile string `yaml:"outputIdentitiesFile"` + MonitoredValues identity.MonitoredValues `yaml:"monitoredValues"` +} diff --git a/cmd/rekor_consistency/main.go b/cmd/rekor_consistency/main.go new file mode 100644 index 00000000..a438df43 --- /dev/null +++ b/cmd/rekor_consistency/main.go @@ -0,0 +1,132 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + "runtime" + "strings" + "time" + + "github.com/sigstore/rekor-monitor/pkg/rekor" + "github.com/sigstore/rekor/pkg/client" + "gopkg.in/yaml.v2" + + "sigs.k8s.io/release-utils/version" +) + +// Default values for monitoring job parameters +const ( + publicRekorServerURL = "https://rekor.sigstore.dev" + logInfoFileName = "logInfo.txt" + outputIdentitiesFileName = "identities.txt" +) + +// This main function performs a periodic root hash consistency check. +// Upon starting, any existing latest snapshot data is loaded and the function runs +// indefinitely to perform consistency check for every time interval that was specified. +func main() { + // Command-line flags that are parameters to the verifier job + configFilePath := flag.String("config-file", "", "Name of the file containing the consistency check workflow configuration settings") + configString := flag.String("config-string", "", "Consistency check workflow configuration settings input as a string") + once := flag.Bool("once", false, "Perform consistency check once and exit") + logInfoFile := flag.String("file", "", "path to log info file") + userAgentString := flag.String("user-agent", "", "details to include in the user agent string") + flag.Parse() + + if *configFilePath == "" && *configString == "" { + log.Fatalf("empty configuration input") + } + + if *configFilePath != "" && *configString != "" { + log.Fatalf("only input one of --config-file or --config-string") + } + + var config ConsistencyCheckConfiguration + if *configString != "" { + if err := yaml.Unmarshal([]byte(*configString), &config); err != nil { + log.Fatalf("error parsing identities: %v", err) + } + } + + if *configFilePath != "" { + readConfig, err := os.ReadFile(*configFilePath) + if err != nil { + log.Fatalf("error reading from identity monitor configuration file: %v", err) + } + if err := yaml.Unmarshal([]byte(readConfig), &config); err != nil { + log.Fatalf("error parsing identities: %v", err) + } + } + + if config.ServerURL == "" { + config.ServerURL = publicRekorServerURL + } + + if config.Interval == nil { + defaultInterval := time.Hour + config.Interval = &defaultInterval + } + + if config.OutputIdentitiesFile == "" { + config.OutputIdentitiesFile = outputIdentitiesFileName + } + + if logInfoFile == nil { + defaultLogInfoFile := logInfoFileName + logInfoFile = &defaultLogInfoFile + } + + rekorClient, err := client.GetRekorClient(config.ServerURL, client.WithUserAgent(strings.TrimSpace(fmt.Sprintf("rekor-monitor/%s (%s; %s) %s", version.GetVersionInfo().GitVersion, runtime.GOOS, runtime.GOARCH, *userAgentString)))) + if err != nil { + log.Fatalf("getting Rekor client: %v", err) + } + + verifier, err := rekor.GetLogVerifier(context.Background(), rekorClient) + if err != nil { + log.Fatal(err) + } + + err = rekor.VerifyConsistencyCheckInputs(config.Interval, logInfoFile, &config.OutputIdentitiesFile, once) + if err != nil { + log.Fatal(err) + } + + ticker := time.NewTicker(*config.Interval) + defer ticker.Stop() + + // Loop will: + // 1. Fetch latest checkpoint and verify + // 2. If old checkpoint is present, verify consistency proof + // 3. Write latest checkpoint to file + + // To get an immediate first tick + for ; ; <-ticker.C { + err = rekor.RunConsistencyCheck(rekorClient, verifier, *logInfoFile, config.MonitoredValues, config.OutputIdentitiesFile, *once) + if err != nil { + fmt.Fprintf(os.Stderr, "error running consistency check: %v", err) + return + } + + if *once { + return + } + } +} diff --git a/cmd/verifier/main.go b/cmd/verifier/main.go deleted file mode 100644 index 5ce979d4..00000000 --- a/cmd/verifier/main.go +++ /dev/null @@ -1,95 +0,0 @@ -// -// Copyright 2021 The Sigstore Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package main - -import ( - "context" - "flag" - "fmt" - "log" - "runtime" - "strings" - "time" - - "github.com/sigstore/rekor-monitor/pkg/identity" - "github.com/sigstore/rekor-monitor/pkg/rekor" - "github.com/sigstore/rekor/pkg/client" - "gopkg.in/yaml.v3" - - "sigs.k8s.io/release-utils/version" -) - -// Default values for monitoring job parameters -const ( - publicRekorServerURL = "https://rekor.sigstore.dev" - logInfoFileName = "logInfo.txt" - outputIdentitiesFileName = "identities.txt" -) - -// This main function performs a periodic root hash consistency check. -// Upon starting, any existing latest snapshot data is loaded and the function runs -// indefinitely to perform consistency check for every time interval that was specified. -func main() { - // Command-line flags that are parameters to the verifier job - serverURL := flag.String("url", publicRekorServerURL, "URL to the rekor server that is to be monitored") - interval := flag.Duration("interval", 5*time.Minute, "Length of interval between each periodical consistency check") - logInfoFile := flag.String("file", logInfoFileName, "Name of the file containing initial merkle tree information") - once := flag.Bool("once", false, "Perform consistency check once and exit") - monitoredValsInput := flag.String("monitored-values", "", "yaml of certificate subjects and issuers, key subjects, "+ - "and fingerprints. For certificates, if no issuers are specified, match any OIDC provider.") - outputIdentitiesFile := flag.String("output-identities", outputIdentitiesFileName, - "Name of the file containing indices and identities found in the log. Format is \"subject issuer index uuid\"") - userAgentString := flag.String("user-agent", "", "details to include in the user agent string") - flag.Parse() - - var monitoredVals identity.MonitoredValues - if err := yaml.Unmarshal([]byte(*monitoredValsInput), &monitoredVals); err != nil { - log.Fatalf("error parsing identities: %v", err) - } - for _, certID := range monitoredVals.CertificateIdentities { - if len(certID.Issuers) == 0 { - fmt.Printf("Monitoring certificate subject %s\n", certID.CertSubject) - } else { - fmt.Printf("Monitoring certificate subject %s for issuer(s) %s\n", certID.CertSubject, strings.Join(certID.Issuers, ",")) - } - } - for _, fp := range monitoredVals.Fingerprints { - fmt.Printf("Monitoring fingerprint %s\n", fp) - } - for _, sub := range monitoredVals.Subjects { - fmt.Printf("Monitoring subject %s\n", sub) - } - - rekorClient, err := client.GetRekorClient(*serverURL, client.WithUserAgent(strings.TrimSpace(fmt.Sprintf("rekor-monitor/%s (%s; %s) %s", version.GetVersionInfo().GitVersion, runtime.GOOS, runtime.GOARCH, *userAgentString)))) - if err != nil { - log.Fatalf("getting Rekor client: %v", err) - } - - verifier, err := rekor.GetLogVerifier(context.Background(), rekorClient) - if err != nil { - log.Fatal(err) - } - - err = rekor.VerifyConsistencyCheckInputs(interval, logInfoFile, outputIdentitiesFile, once) - if err != nil { - log.Fatal(err) - } - - err = rekor.RunConsistencyCheck(*interval, rekorClient, verifier, *logInfoFile, monitoredVals, *outputIdentitiesFile, *once) - if err != nil { - log.Fatalf("%v", err) - } -} diff --git a/pkg/rekor/verifier.go b/pkg/rekor/verifier.go index 4864465f..3f96aaee 100644 --- a/pkg/rekor/verifier.go +++ b/pkg/rekor/verifier.go @@ -111,62 +111,53 @@ func VerifyConsistencyCheckInputs(interval *time.Duration, logInfoFile *string, } // RunConsistencyCheck periodically verifies the root hash consistency of a Rekor log. -func RunConsistencyCheck(interval time.Duration, rekorClient *client.Rekor, verifier signature.Verifier, logInfoFile string, mvs identity.MonitoredValues, outputIdentitiesFile string, once bool) error { - ticker := time.NewTicker(interval) - defer ticker.Stop() - - // Loop will: - // 1. Fetch latest checkpoint and verify - // 2. If old checkpoint is present, verify consistency proof - // 3. Write latest checkpoint to file - - // To get an immediate first tick - for ; ; <-ticker.C { - logInfo, err := GetLogInfo(context.Background(), rekorClient) - if err != nil { - return fmt.Errorf("failed to get log info: %v", err) - } - checkpoint, err := verifyLatestCheckpointSignature(logInfo, verifier) +func RunConsistencyCheck(rekorClient *client.Rekor, verifier signature.Verifier, logInfoFile string, mvs identity.MonitoredValues, outputIdentitiesFile string, once bool) error { + logInfo, err := GetLogInfo(context.Background(), rekorClient) + if err != nil { + return fmt.Errorf("failed to get log info: %v", err) + } + checkpoint, err := verifyLatestCheckpointSignature(logInfo, verifier) + if err != nil { + return fmt.Errorf("failed to verify signature of latest checkpoint: %v", err) + } + + fi, err := os.Stat(logInfoFile) + // File containing previous checkpoints exists + var prevCheckpoint *util.SignedCheckpoint + if err == nil && fi.Size() != 0 { + prevCheckpoint, err = verifyCheckpointConsistency(logInfoFile, checkpoint, *logInfo.TreeID, rekorClient, verifier) if err != nil { - return fmt.Errorf("failed to verify signature of latest checkpoint: %v", err) + return fmt.Errorf("failed to verify previous checkpoint: %v", err) } - fi, err := os.Stat(logInfoFile) - // File containing previous checkpoints exists - var prevCheckpoint *util.SignedCheckpoint - if err == nil && fi.Size() != 0 { - prevCheckpoint, err = verifyCheckpointConsistency(logInfoFile, checkpoint, *logInfo.TreeID, rekorClient, verifier) - if err != nil { - return fmt.Errorf("failed to verify previous checkpoint: %v", err) - } - - } + } - // Write if there was no stored checkpoint or the sizes differ - if prevCheckpoint == nil || prevCheckpoint.Size != checkpoint.Size { - if err := file.WriteCheckpoint(checkpoint, logInfoFile); err != nil { - // TODO: Once the consistency check and identity search are split into separate tasks, this should hard fail. - // Temporarily skipping this to allow this job to succeed, remediating the issue noted here: https://github.com/sigstore/rekor-monitor/issues/271 - fmt.Fprintf(os.Stderr, "failed to write checkpoint: %v", err) - } + // Write if there was no stored checkpoint or the sizes differ + if prevCheckpoint == nil || prevCheckpoint.Size != checkpoint.Size { + if err := file.WriteCheckpoint(checkpoint, logInfoFile); err != nil { + // TODO: Once the consistency check and identity search are split into separate tasks, this should hard fail. + // Temporarily skipping this to allow this job to succeed, remediating the issue noted here: https://github.com/sigstore/rekor-monitor/issues/271 + fmt.Fprintf(os.Stderr, "failed to write checkpoint: %v", err) } + } - if prevCheckpoint != nil && prevCheckpoint.Size != checkpoint.Size { - err = writeIdentitiesBetweenCheckpoints(logInfo, prevCheckpoint, checkpoint, mvs, rekorClient, outputIdentitiesFile) - if err != nil { - return fmt.Errorf("failed to monitor identities: %v", err) - } + if prevCheckpoint != nil && prevCheckpoint.Size != checkpoint.Size { + err = writeIdentitiesBetweenCheckpoints(logInfo, prevCheckpoint, checkpoint, mvs, rekorClient, outputIdentitiesFile) + if err != nil { + return fmt.Errorf("failed to monitor identities: %v", err) } + } - // TODO: Switch to writing checkpoints to GitHub so that the history is preserved. Then we only need - // to persist the last checkpoint. - // Delete old checkpoints to avoid the log growing indefinitely - if err := file.DeleteOldCheckpoints(logInfoFile); err != nil { - return fmt.Errorf("failed to delete old checkpoints: %v", err) - } + // TODO: Switch to writing checkpoints to GitHub so that the history is preserved. Then we only need + // to persist the last checkpoint. + // Delete old checkpoints to avoid the log growing indefinitely + if err := file.DeleteOldCheckpoints(logInfoFile); err != nil { + return fmt.Errorf("failed to delete old checkpoints: %v", err) + } - if once { - return nil - } + if once { + return nil } + + return nil }