Compare commits
11 Commits
cbfa97d480
...
custom-dom
Author | SHA1 | Date | |
---|---|---|---|
d2c564a000 | |||
|
6bc0071bdb | ||
|
a044cb86bd | ||
|
6ecc5d7784 | ||
|
ef0970044b | ||
|
38fe915b37 | ||
|
c9c1e81619 | ||
|
92e3f6baac | ||
|
0ef1a18331 | ||
|
849dacdc30 | ||
|
20607b54d5 |
41
README.md
41
README.md
@@ -1,6 +1,6 @@
|
||||
# Cheese
|
||||
|
||||
A Certificate Transparency log configuration and deployment tool.
|
||||
A Certificate Transparency log configuration and deployment tool for Google's [[TesseraCT](github.com/transparency-dev/tesseract)] implementation. It tries to look and feel a little like the one provided by [[Sunlight](https://github.com/FiloSottile/sunlight)].
|
||||
|
||||
## Configuration Generator
|
||||
|
||||
@@ -9,14 +9,20 @@ in a very similar way to Sunlight.
|
||||
|
||||
### Usage
|
||||
|
||||
1. **Create YAML configuration file:**
|
||||
1. **Build the tool:**
|
||||
```bash
|
||||
go build -o tesseract-genconf ./tesseract/genconf/
|
||||
```
|
||||
|
||||
2. **Create YAML configuration file:**
|
||||
|
||||
```yaml
|
||||
listen:
|
||||
- "[::]:16420"
|
||||
- "[::]:8080"
|
||||
roots: /etc/tesseract/roots.pem
|
||||
logs:
|
||||
- shortname: example2025h1
|
||||
listen: "[::]:16900"
|
||||
inception: 2025-01-01
|
||||
submissionprefix: https://example2025h1.log.ct.example.com
|
||||
monitoringprefix: https://example2025h1.mon.ct.example.com
|
||||
@@ -27,25 +33,36 @@ logs:
|
||||
notafterlimit: 2025-07-01T00:00:00Z
|
||||
```
|
||||
|
||||
2. **Generate private keys:**
|
||||
3. **Generate private keys:**
|
||||
```bash
|
||||
go run ./tesseract/genconf/main.go -c config.yaml gen-key
|
||||
mkdir -p /etc/tesseract/keys
|
||||
./tesseract-genconf -c config.yaml gen-key
|
||||
```
|
||||
|
||||
3. **Create directories and generate environment files:**
|
||||
4. **Create directories and generate environment files:**
|
||||
```bash
|
||||
mkdir -p /var/lib/tesseract/example2025h1/data
|
||||
go run ./tesseract/genconf/main.go -c config.yaml gen-env
|
||||
./tesseract-genconf -c config.yaml gen-env
|
||||
```
|
||||
|
||||
4. **Generate HTML and JSON files:**
|
||||
5. **Generate HTML and JSON files:**
|
||||
```bash
|
||||
go run ./tesseract/genconf/main.go -c config.yaml gen-html
|
||||
./tesseract-genconf -c config.yaml gen-html
|
||||
```
|
||||
|
||||
5. **Generate nginx configuration files:**
|
||||
6. **Generate nginx configuration files:**
|
||||
```bash
|
||||
go run ./tesseract/genconf/main.go -c config.yaml gen-nginx
|
||||
./tesseract-genconf -c config.yaml gen-nginx
|
||||
```
|
||||
|
||||
You can symlink the generated $monitoringprefix.conf files from `/etc/nginx/sites-enabled/`
|
||||
The port from the main `listen:` field will be used in the NGINX server blocks (in our case
|
||||
`:8080`). You can symlink the generated $monitoringprefix.conf files from `/etc/nginx/sites-enabled/`.
|
||||
|
||||
7. **Generate root certificates (optional):**
|
||||
```bash
|
||||
# For testing/staging environment, take the ccadb 'testing' roots
|
||||
./tesseract-genconf gen-roots --source https://rennet2027h2.log.ct.ipng.ch/ --output roots-staging.pem
|
||||
|
||||
# For production environment, take the ccadb 'production' roots
|
||||
./tesseract-genconf gen-roots --source https://gouda2027h2.log.ct.ipng.ch/ --output roots-production.pem
|
||||
```
|
||||
|
6380
production-roots.pem
Normal file
6380
production-roots.pem
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -25,16 +24,15 @@ func generateEnv(yamlFile string) {
|
||||
|
||||
// Create combined roots.pem file
|
||||
rootsPemPath := filepath.Join(logEntry.LocalDirectory, "roots.pem")
|
||||
err := createCombinedRootsPem(config.Roots, logEntry.ExtraRoots, rootsPemPath)
|
||||
err := createCombinedRootsPemWithStatus(config.Roots, logEntry.ExtraRoots, rootsPemPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create %s: %v", rootsPemPath, err)
|
||||
}
|
||||
fmt.Printf("Generated %s\n", rootsPemPath)
|
||||
|
||||
// Build TESSERACT_ARGS string
|
||||
args := []string{
|
||||
fmt.Sprintf("--private_key=%s", logEntry.Secret),
|
||||
fmt.Sprintf("--origin=%s.log.ct.ipng.ch", logEntry.ShortName),
|
||||
fmt.Sprintf("--origin=%s.%s", logEntry.ShortName, logEntry.Domain),
|
||||
fmt.Sprintf("--storage_dir=%s", logEntry.LocalDirectory),
|
||||
fmt.Sprintf("--roots_pem_file=%s", rootsPemPath),
|
||||
}
|
||||
@@ -44,53 +42,43 @@ func generateEnv(yamlFile string) {
|
||||
args = append(args, fmt.Sprintf("--http_endpoint=%s", logEntry.Listen))
|
||||
}
|
||||
|
||||
// Add not_after flags if specified
|
||||
if !logEntry.NotAfterStart.IsZero() {
|
||||
args = append(args, fmt.Sprintf("--not_after_start=%s", logEntry.NotAfterStart.Format("2006-01-02T15:04:05Z")))
|
||||
}
|
||||
if !logEntry.NotAfterLimit.IsZero() {
|
||||
args = append(args, fmt.Sprintf("--not_after_limit=%s", logEntry.NotAfterLimit.Format("2006-01-02T15:04:05Z")))
|
||||
}
|
||||
|
||||
tesseractArgs := strings.Join(args, " ")
|
||||
envContent := fmt.Sprintf("TESSERACT_ARGS=\"%s\"\nOTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318\n", tesseractArgs)
|
||||
|
||||
err = os.WriteFile(envPath, []byte(envContent), 0644)
|
||||
err = writeFileWithStatus(envPath, []byte(envContent))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to write %s: %v", envPath, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Generated %s\n", envPath)
|
||||
}
|
||||
}
|
||||
|
||||
func createCombinedRootsPem(rootsFile, extraRootsFile, outputPath string) error {
|
||||
// Create output file
|
||||
outputFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: %v", err)
|
||||
}
|
||||
defer outputFile.Close()
|
||||
|
||||
// Copy main roots file
|
||||
func createCombinedRootsPemWithStatus(rootsFile, extraRootsFile, outputPath string) error {
|
||||
// Read main roots file
|
||||
var combinedContent []byte
|
||||
if rootsFile != "" {
|
||||
rootsData, err := os.Open(rootsFile)
|
||||
rootsData, err := os.ReadFile(rootsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open roots file %s: %v", rootsFile, err)
|
||||
}
|
||||
defer rootsData.Close()
|
||||
|
||||
_, err = io.Copy(outputFile, rootsData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy roots file: %v", err)
|
||||
return fmt.Errorf("failed to read roots file %s: %v", rootsFile, err)
|
||||
}
|
||||
combinedContent = append(combinedContent, rootsData...)
|
||||
}
|
||||
|
||||
// Append extra roots file if it exists
|
||||
if extraRootsFile != "" {
|
||||
extraRootsData, err := os.Open(extraRootsFile)
|
||||
extraRootsData, err := os.ReadFile(extraRootsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open extra roots file %s: %v", extraRootsFile, err)
|
||||
}
|
||||
defer extraRootsData.Close()
|
||||
|
||||
_, err = io.Copy(outputFile, extraRootsData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy extra roots file: %v", err)
|
||||
return fmt.Errorf("failed to read extra roots file %s: %v", extraRootsFile, err)
|
||||
}
|
||||
combinedContent = append(combinedContent, extraRootsData...)
|
||||
}
|
||||
|
||||
return nil
|
||||
return writeFileWithStatus(outputPath, combinedContent)
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
@@ -58,25 +59,28 @@ const htmlTemplate = `<!DOCTYPE html>
|
||||
<body>
|
||||
<div class="container">
|
||||
<p align="center">
|
||||
<img alt="A TesseraCT logo, a stylized four-dimensional hypercube with on the left side penstrokes of orange with a padlock, and on the right penstrokes of dark blue with a magnifying glass." width="250" height="278" src="https://ipng.ch/assets/ctlog/tesseract-logo.png"> </p>
|
||||
<img alt="A TesseraCT logo, a stylized four-dimensional hypercube with on the left side penstrokes of orange with a padlock, and on the right penstrokes of dark blue with a magnifying glass." width="260" height="250" src="https://ipng.ch/assets/ctlog/tesseract-logo.png"> </p>
|
||||
|
||||
<p>
|
||||
This is a <a href="https://github.com/transparency-dev/tesseract">TesseraCT</a> Certificate Transparency log instance.
|
||||
|
||||
<p>
|
||||
<a href="/metrics">Metrics</a> are available.
|
||||
|
||||
<hr>
|
||||
|
||||
<p>
|
||||
The following logs are active.
|
||||
The following logs are active:
|
||||
|
||||
{{range .Logs}}
|
||||
|
||||
<h2>{{.ShortName}}.log.ct.ipng.ch</h2>
|
||||
<h2>{{.ShortName}}.{{.Domain}}</h2>
|
||||
|
||||
<p>
|
||||
Log ID: <code>{{.LogID}}</code><br>
|
||||
Monitoring prefix: <code>{{.MonitoringPrefix}}/</code><br>
|
||||
Submission prefix: <code>{{.SubmissionPrefix}}/</code><br>
|
||||
Interval: {{.NotAfterStart.Format "2006-01-02T15:04:05Z"}} – {{.NotAfterLimit.Format "2006-01-02T15:04:05Z"}}<br>
|
||||
Interval: {{.NotAfterStart.Format "2006-01-02T15:04:05Z"}} - {{.NotAfterLimit.Format "2006-01-02T15:04:05Z"}}<br>
|
||||
Links: <a href="{{.MonitoringPrefix}}/checkpoint">checkpoint</a>
|
||||
<a href="data:application/octet-stream;base64,{{.PublicKeyDERB64}}"
|
||||
download="{{.ShortName}}.der">key</a>
|
||||
@@ -133,27 +137,25 @@ func generateHTML(yamlFile string) {
|
||||
for _, logEntry := range config.Logs {
|
||||
indexPath := fmt.Sprintf("%s/index.html", logEntry.LocalDirectory)
|
||||
|
||||
file, err := os.Create(indexPath)
|
||||
// Execute template to buffer
|
||||
var buf bytes.Buffer
|
||||
err := tmpl.Execute(&buf, config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create %s: %v", indexPath, err)
|
||||
log.Fatalf("Failed to execute HTML template for %s: %v", indexPath, err)
|
||||
}
|
||||
|
||||
err = tmpl.Execute(file, config)
|
||||
// Write file with status
|
||||
err = writeFileWithStatus(indexPath, buf.Bytes())
|
||||
if err != nil {
|
||||
file.Close()
|
||||
log.Fatalf("Failed to write HTML to %s: %v", indexPath, err)
|
||||
}
|
||||
|
||||
file.Close()
|
||||
fmt.Printf("Generated %s\n", indexPath)
|
||||
|
||||
// Generate log.v3.json for this log
|
||||
jsonPath := filepath.Join(logEntry.LocalDirectory, "log.v3.json")
|
||||
err = generateLogJSON(logEntry, jsonPath)
|
||||
err = generateLogJSONWithStatus(logEntry, jsonPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate %s: %v", jsonPath, err)
|
||||
}
|
||||
fmt.Printf("Generated %s\n", jsonPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,9 +209,9 @@ func computeKeyInfo(logEntry *Log) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateLogJSON(logEntry Log, outputPath string) error {
|
||||
func generateLogJSONWithStatus(logEntry Log, outputPath string) error {
|
||||
logJSON := LogV3JSON{
|
||||
Description: fmt.Sprintf("%s.log.ct.ipng.ch", logEntry.ShortName),
|
||||
Description: fmt.Sprintf("%s.%s", logEntry.ShortName, logEntry.Domain),
|
||||
SubmissionURL: fmt.Sprintf("%s/", logEntry.SubmissionPrefix),
|
||||
MonitoringURL: fmt.Sprintf("%s/", logEntry.MonitoringPrefix),
|
||||
TemporalInterval: TemporalInterval{
|
||||
@@ -226,10 +228,5 @@ func generateLogJSON(logEntry Log, outputPath string) error {
|
||||
return fmt.Errorf("failed to marshal JSON: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(outputPath, jsonData, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write JSON file: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return writeFileWithStatus(outputPath, jsonData)
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ type Config struct {
|
||||
|
||||
type Log struct {
|
||||
ShortName string `yaml:"shortname"`
|
||||
Domain string `yaml:"domain"`
|
||||
Inception string `yaml:"inception"`
|
||||
Period int `yaml:"period"`
|
||||
PoolSize int `yaml:"poolsize"`
|
||||
@@ -79,19 +80,55 @@ func loadConfig(yamlFile string) Config {
|
||||
log.Fatalf("Failed to parse YAML: %v", err)
|
||||
}
|
||||
|
||||
// Set defaults for log entries
|
||||
for i := range config.Logs {
|
||||
if config.Logs[i].PoolSize == 0 {
|
||||
config.Logs[i].PoolSize = 750
|
||||
// Set default listen port if not configured
|
||||
if len(config.Listen) == 0 {
|
||||
config.Listen = []string{":8080"}
|
||||
}
|
||||
|
||||
// Set defaults for log entries and check for empty/missing values
|
||||
for i := range config.Logs {
|
||||
// Checks are in order of fields of the Log struct
|
||||
|
||||
if config.Logs[i].ShortName == "" {
|
||||
log.Fatalf("Log %d is missing a ShortName", i)
|
||||
}
|
||||
|
||||
if config.Logs[i].Domain == "" {
|
||||
log.Fatalf("Log %d (%s) is missing a value for Domain", i, config.Logs[i].ShortName)
|
||||
}
|
||||
|
||||
if config.Logs[i].Period == 0 {
|
||||
config.Logs[i].Period = 200
|
||||
}
|
||||
|
||||
if config.Logs[i].PoolSize == 0 {
|
||||
config.Logs[i].PoolSize = 750
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func writeFileWithStatus(filename string, content []byte) error {
|
||||
existingContent, err := os.ReadFile(filename)
|
||||
if os.IsNotExist(err) {
|
||||
fmt.Printf("Creating %s\n", filename)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to read existing file %s: %v", filename, err)
|
||||
} else if string(existingContent) == string(content) {
|
||||
fmt.Printf("Unchanged %s\n", filename)
|
||||
return nil
|
||||
} else {
|
||||
fmt.Printf("Updating %s\n", filename)
|
||||
}
|
||||
|
||||
err = os.WriteFile(filename, content, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write file %s: %v", filename, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func showHelp() {
|
||||
fmt.Printf("Usage: %s [options] <command>\n\n", os.Args[0])
|
||||
fmt.Printf("Options:\n")
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -10,8 +11,8 @@ import (
|
||||
)
|
||||
|
||||
const nginxTemplate = `server {
|
||||
listen 8080;
|
||||
listen [::]:8080;
|
||||
listen {{.ListenPort}};
|
||||
listen [::]:{{.ListenPort}};
|
||||
|
||||
# Replace with your actual domain(s)
|
||||
server_name {{.MonitoringHost}};
|
||||
@@ -72,11 +73,21 @@ const nginxTemplate = `server {
|
||||
type NginxTemplateData struct {
|
||||
MonitoringHost string
|
||||
LocalDirectory string
|
||||
ListenPort string
|
||||
}
|
||||
|
||||
func generateNginx(yamlFile string) {
|
||||
config := loadConfig(yamlFile)
|
||||
|
||||
// Extract port from first listen address
|
||||
listenPort := "8080" // fallback default
|
||||
if len(config.Listen) > 0 {
|
||||
port := extractPort(config.Listen[0])
|
||||
if port != "" {
|
||||
listenPort = port
|
||||
}
|
||||
}
|
||||
|
||||
for _, log := range config.Logs {
|
||||
// Extract hostname from monitoring prefix
|
||||
hostname, err := extractHostname(log.MonitoringPrefix)
|
||||
@@ -89,6 +100,7 @@ func generateNginx(yamlFile string) {
|
||||
data := NginxTemplateData{
|
||||
MonitoringHost: hostname,
|
||||
LocalDirectory: log.LocalDirectory,
|
||||
ListenPort: listenPort,
|
||||
}
|
||||
|
||||
// Parse and execute template
|
||||
@@ -102,24 +114,41 @@ func generateNginx(yamlFile string) {
|
||||
outputFilename := fmt.Sprintf("%s.conf", hostname)
|
||||
outputPath := filepath.Join(log.LocalDirectory, outputFilename)
|
||||
|
||||
// Create output file
|
||||
file, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to create nginx config file %s: %v\n", outputPath, err)
|
||||
continue
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Execute template
|
||||
err = tmpl.Execute(file, data)
|
||||
// Execute template to buffer
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, data)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to execute nginx template for %s: %v\n", outputPath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("Generated nginx config: %s\n", outputPath)
|
||||
// Write file with status
|
||||
err = writeFileWithStatus(outputPath, buf.Bytes())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write nginx config file %s: %v\n", outputPath, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractPort(listenAddr string) string {
|
||||
// Handle common listen address formats:
|
||||
// ":8080" -> "8080"
|
||||
// "localhost:8080" -> "8080"
|
||||
// "[::]:8080" -> "8080"
|
||||
|
||||
if strings.HasPrefix(listenAddr, ":") {
|
||||
return listenAddr[1:] // Remove the leading ":"
|
||||
}
|
||||
|
||||
// For addresses with host:port format
|
||||
if strings.Contains(listenAddr, ":") {
|
||||
parts := strings.Split(listenAddr, ":")
|
||||
return parts[len(parts)-1] // Return the last part (port)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractHostname(urlStr string) (string, error) {
|
||||
if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -8,7 +9,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -67,15 +67,10 @@ func generateRoots(args []string) {
|
||||
log.Fatalf("Failed to parse JSON response: %v", err)
|
||||
}
|
||||
|
||||
// Create output file
|
||||
outFile, err := os.Create(outputFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create output file %s: %v", outputFile, err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
// Write each certificate as PEM
|
||||
// Collect all valid certificates in a buffer
|
||||
var pemBuffer bytes.Buffer
|
||||
validCertCount := 0
|
||||
|
||||
for _, certBase64 := range rootsResp.Certificates {
|
||||
// Decode base64 certificate
|
||||
certBytes, err := base64.StdEncoding.DecodeString(certBase64)
|
||||
@@ -102,14 +97,20 @@ func generateRoots(args []string) {
|
||||
Bytes: certBytes,
|
||||
}
|
||||
|
||||
// Write PEM to file
|
||||
err = pem.Encode(outFile, pemBlock)
|
||||
// Write PEM to buffer
|
||||
err = pem.Encode(&pemBuffer, pemBlock)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to write PEM certificate: %v", err)
|
||||
log.Fatalf("Failed to encode PEM certificate: %v", err)
|
||||
}
|
||||
|
||||
validCertCount++
|
||||
}
|
||||
|
||||
// Write all certificates to file with status
|
||||
err = writeFileWithStatus(outputFile, pemBuffer.Bytes())
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to write output file %s: %v", outputFile, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully wrote %d certificates to %s (out of %d total)\n", validCertCount, outputFile, len(rootsResp.Certificates))
|
||||
}
|
||||
|
Reference in New Issue
Block a user