Configuration file reading
* An example configuration file has been added. * The data structure that corresponds to the configuration has been defined, and functions to load it have been added. * Logging using logrus and command line arguments that configure logging and set the configuration file's path have been added. * Opening the UNIX socket has been implemented.
This commit is contained in:
parent
18ce1d6738
commit
610cbf28f8
7 changed files with 407 additions and 1 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
||||||
bin
|
bin
|
||||||
go.sum
|
go.sum
|
||||||
|
fetchcert
|
||||||
|
fetch-certificates.yml
|
||||||
|
|
104
config.go
Normal file
104
config.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
/* *
|
||||||
|
* CONFIGURATION DATA *
|
||||||
|
* */
|
||||||
|
|
||||||
|
// UNIX socket configuration. This includes the full path to the socket
|
||||||
|
// as well as the group name and mode.
|
||||||
|
tSocketConfig struct {
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Group string `yaml:"group"`
|
||||||
|
Mode os.FileMode `yaml:"mode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LDAP connection configuration, used for servers and as a way to specify
|
||||||
|
// defaults.
|
||||||
|
tLdapConnectionConfig struct {
|
||||||
|
Port uint16 `yaml:"port"`
|
||||||
|
TLS string `yaml:"tls"`
|
||||||
|
TLSNoVerify bool `yaml:"tls_skip_verify"`
|
||||||
|
CaChain string `yaml:"ca_chain"`
|
||||||
|
BindUser string `yaml:"bind_user"`
|
||||||
|
BindPassword string `yaml:"bind_password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LDAP server configuration. This defines how to connect to a
|
||||||
|
// single, specific LDAP server.
|
||||||
|
tLdapServerConfig struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
tLdapConnectionConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// LDAP attributes and base DN configuration
|
||||||
|
tLdapStructureConfig struct {
|
||||||
|
BaseDN string `yaml:"base_dn"`
|
||||||
|
EndEntityCertificate string `yaml:"end_entity"`
|
||||||
|
CACertificate string `yaml:"ca_certificate"`
|
||||||
|
CAChaining string `yaml:"ca_chaining"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LDAP configuration: LDAP structure, connection defaults and server
|
||||||
|
// connections.
|
||||||
|
tLdapConfig struct {
|
||||||
|
Structure tLdapStructureConfig `yaml:"structure"`
|
||||||
|
Defaults tLdapConnectionConfig `yaml:"defaults"`
|
||||||
|
Servers []tLdapServerConfig `yaml:"servers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certificate file configuration.
|
||||||
|
tCertificateFileConfig struct {
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Mode os.FileMode `yaml:"mode"`
|
||||||
|
Owner string `yaml:"owner"`
|
||||||
|
Group string `yaml:"group"`
|
||||||
|
PrependFiles []string `yaml:"prepend_files"`
|
||||||
|
Certificate string `yaml:"certificate"`
|
||||||
|
CACertificates []string `yaml:"ca"`
|
||||||
|
CAChainOf string `yaml:"ca_chain_of"`
|
||||||
|
Reverse bool `yaml:"reverse"`
|
||||||
|
AppendFiles []string `yaml:"append_files"`
|
||||||
|
AfterUpdate []string `yaml:"after_update"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main configuration.
|
||||||
|
tConfiguration struct {
|
||||||
|
Socket tSocketConfig `yaml:"socket"`
|
||||||
|
LdapConfig tLdapConfig `yaml:"ldap"`
|
||||||
|
Certificates []tCertificateFileConfig `yaml:"certificates"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func defaultConfiguration() tConfiguration {
|
||||||
|
cfg := tConfiguration{}
|
||||||
|
cfg.Socket.Mode = 0640
|
||||||
|
cfg.LdapConfig.Defaults.Port = 389
|
||||||
|
cfg.LdapConfig.Defaults.TLS = "no"
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and check the configuration file
|
||||||
|
func loadConfiguration(file string) (tConfiguration, error) {
|
||||||
|
cfg := defaultConfiguration()
|
||||||
|
|
||||||
|
cfgData, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, fmt.Errorf("Could not load configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = yaml.Unmarshal(cfgData, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, fmt.Errorf("Could not parse configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
95
fetch-certificates.yml.example
Normal file
95
fetch-certificates.yml.example
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
# fetchcert configuration example / documentation
|
||||||
|
# ===============================================
|
||||||
|
|
||||||
|
# The UNIX socket the main program listens on.
|
||||||
|
socket:
|
||||||
|
# The path to the UNIX socket.
|
||||||
|
path: /tmp/socket
|
||||||
|
# A group name to set as the socket's owner. No group change will occur if
|
||||||
|
# this entry is not set.
|
||||||
|
group: users
|
||||||
|
# The socket's access mode. Defaults to 0640.
|
||||||
|
mode: 0640
|
||||||
|
|
||||||
|
# Configuration for the LDAP servers and structure.
|
||||||
|
ldap:
|
||||||
|
|
||||||
|
structure:
|
||||||
|
# Base DN that will be appended to certificate DNs
|
||||||
|
base_dn: ou=certificates,dc=example,dc=org
|
||||||
|
# Name of the attribute that will contain an end entity certificate
|
||||||
|
# in the LDAP objects.
|
||||||
|
end_entity: userCertificate
|
||||||
|
# Name of the attribute that will contain a CA certificate in the LDAP
|
||||||
|
# objects.
|
||||||
|
ca_certificate: caCertificate
|
||||||
|
# Attribute that will contain the DN of the next certificate in the chain.
|
||||||
|
ca_chaining: seeAlso
|
||||||
|
|
||||||
|
# These are the defaults for the LDAP server connections. May be completely
|
||||||
|
# omitted.
|
||||||
|
defaults:
|
||||||
|
# Port number - usually 389 for clear/starttls or 636 for TLS. Defaults to
|
||||||
|
# 389.
|
||||||
|
port: 636
|
||||||
|
# TLS mode. This must be either "yes" for the non-standard, pure TLS mode,
|
||||||
|
# "starttls" for TLS over a clear connection, or "no" to use a clear
|
||||||
|
# connection. Defaults to "no".
|
||||||
|
tls: yes
|
||||||
|
# Skip server certificate check. Defaults to false.
|
||||||
|
tls_skip_verify: false
|
||||||
|
# CA certificate chain. Can be omitted if the systems' trusted CAs must be
|
||||||
|
# used, or if no TLS is being used.
|
||||||
|
ca_chain: /path/to/ca/chain.pem
|
||||||
|
# LDAP user (as a DN) and password to bind with. Both fields may be
|
||||||
|
# omitted if anonymous binds are to be used.
|
||||||
|
bind_user: cn=fetchcert,ou=automation,dc=example,dc=org
|
||||||
|
bind_password: drowssap
|
||||||
|
|
||||||
|
# Configurations for each LDAP server. Each entry must incluse a "host"
|
||||||
|
# field which defines the host name for the server ; it may also redefine
|
||||||
|
# any of the defaults above.
|
||||||
|
servers:
|
||||||
|
- host: ldap1.example.org
|
||||||
|
- host: ldap2.example.org
|
||||||
|
|
||||||
|
# Certificates that must be updated
|
||||||
|
certificates:
|
||||||
|
|
||||||
|
# Path to the file to generate
|
||||||
|
- path: /etc/ssl/private/cert1.pem
|
||||||
|
# Access mode, owner and group for the file. May be omitted.
|
||||||
|
mode: 0640
|
||||||
|
owner: root
|
||||||
|
group: somegroup
|
||||||
|
# A list of files to prepend. Can be used to e.g. copy the private key
|
||||||
|
# into this file.
|
||||||
|
prepend_files:
|
||||||
|
- /some/file.pem
|
||||||
|
# DN of the certificate itself. If a base DN is defined in the LDAP
|
||||||
|
# section, it will be appended to this value. Can be omitted if either
|
||||||
|
# the ca or ca_chain_of fields below are in use.
|
||||||
|
certificate: cn=www.example.org,ou=webservers
|
||||||
|
# A list of DNs of CA certificates. The base DN from the LDAP section will
|
||||||
|
# be appended to each entry if defined. If this list is empty and the
|
||||||
|
# ca_chain_of field below is undefined as well, the certificate field
|
||||||
|
# above must be defined.
|
||||||
|
ca: ['cn=root,ou=ca','cn=interm,ou=ca']
|
||||||
|
# Alternatively, CA chaining using the LDAP attribute defined above can
|
||||||
|
# be used by specifying the DN of a certificate here. The certificate
|
||||||
|
# matching the DN will be ignored, it will only be used as the start of
|
||||||
|
# the chain. Using this mechanism is incompatible with usage of the ca
|
||||||
|
# field above.
|
||||||
|
ca_chain_of: cn=www.example.org,ou=webservers
|
||||||
|
# Reverse order. If this is false, the main certificate will be written
|
||||||
|
# first, followed by the first intermediary certificate, and so on until
|
||||||
|
# the root CA certificate is found. If this is true, the first certificate
|
||||||
|
# in the file will be the root CA certificate.
|
||||||
|
reverse: false
|
||||||
|
# A list of files to append to the output.
|
||||||
|
append_files:
|
||||||
|
- /some/other/file.pem
|
||||||
|
# A list of commands that will be executed when the file is replaced.
|
||||||
|
# If one of the commands fails, execution will stop.
|
||||||
|
after_update:
|
||||||
|
- apache2ctl graceful
|
12
go.mod
12
go.mod
|
@ -1,3 +1,15 @@
|
||||||
module nocternity.net/go/fetchcert
|
module nocternity.net/go/fetchcert
|
||||||
|
|
||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gemnasium/logrus-graylog-hook/v3 v3.0.3
|
||||||
|
github.com/karrick/golf v1.4.0
|
||||||
|
github.com/sirupsen/logrus v1.8.1
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/pkg/errors v0.8.1 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
|
||||||
|
)
|
||||||
|
|
73
logging.go
Normal file
73
logging.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log/syslog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
lrh_gl "github.com/gemnasium/logrus-graylog-hook/v3"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
lrh_sl "github.com/sirupsen/logrus/hooks/syslog"
|
||||||
|
lrh_wr "github.com/sirupsen/logrus/hooks/writer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// The logging context.
|
||||||
|
log *logrus.Entry
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configure the log level
|
||||||
|
func toLogLevel(cliLevel string) logrus.Level {
|
||||||
|
if cliLevel == "" {
|
||||||
|
return logrus.InfoLevel
|
||||||
|
}
|
||||||
|
lvl, err := logrus.ParseLevel(cliLevel)
|
||||||
|
if err == nil {
|
||||||
|
return lvl
|
||||||
|
}
|
||||||
|
log.WithField("level", cliLevel).Warning("Invalid log level on command line")
|
||||||
|
return logrus.InfoLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a file writer hook to the logging library.
|
||||||
|
func configureLogFile(path string) {
|
||||||
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err == nil {
|
||||||
|
log.Logger.AddHook(&lrh_wr.Hook{
|
||||||
|
Writer: file,
|
||||||
|
LogLevels: logrus.AllLevels,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"error": err,
|
||||||
|
"file": path,
|
||||||
|
}).Error("Could not open log file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the logging library based on the various command line flags.
|
||||||
|
func configureLogging(flags cliFlags) error {
|
||||||
|
log = logrus.NewEntry(logrus.New())
|
||||||
|
log.Logger.SetFormatter(&logrus.TextFormatter{
|
||||||
|
DisableColors: true,
|
||||||
|
FullTimestamp: true,
|
||||||
|
})
|
||||||
|
log.Logger.SetLevel(toLogLevel(flags.logLevel))
|
||||||
|
if flags.logFile != "" {
|
||||||
|
configureLogFile(flags.logFile)
|
||||||
|
}
|
||||||
|
if flags.logGraylog != "" {
|
||||||
|
log.Logger.AddHook(lrh_gl.NewGraylogHook(flags.logGraylog, nil))
|
||||||
|
}
|
||||||
|
if flags.logSyslog {
|
||||||
|
hook, err := lrh_sl.NewSyslogHook("", "", syslog.LOG_DEBUG, "fetchcert")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Logger.AddHook(hook)
|
||||||
|
}
|
||||||
|
if flags.quiet {
|
||||||
|
log.Logger.SetOutput(ioutil.Discard)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
73
main.go
Normal file
73
main.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/karrick/golf"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// This structure contains all values that may be set from the command line.
|
||||||
|
cliFlags struct {
|
||||||
|
// The path to the configuration file.
|
||||||
|
cfgFile string
|
||||||
|
// Quiet mode. Will disable logging to stderr.
|
||||||
|
quiet bool
|
||||||
|
// The log level.
|
||||||
|
logLevel string
|
||||||
|
// A file to write logs into.
|
||||||
|
logFile string
|
||||||
|
// Graylog server to send logs to (using GELF/UDP). Format is <hostname>:<port>.
|
||||||
|
logGraylog string
|
||||||
|
// Send logs to syslog.
|
||||||
|
logSyslog bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse command line options.
|
||||||
|
func parseCommandLine() cliFlags {
|
||||||
|
var help bool
|
||||||
|
flags := cliFlags{}
|
||||||
|
|
||||||
|
golf.StringVarP(&flags.cfgFile, 'c', "config", "/etc/fetch-certificates.yml", "Path to the configuration file.")
|
||||||
|
golf.StringVarP(&flags.logFile, 'f', "log-file", "", "Path to the log file.")
|
||||||
|
golf.StringVarP(&flags.logGraylog, 'g', "log-graylog", "", "Log to Graylog server (format: <host>:<port>).")
|
||||||
|
golf.BoolVarP(&help, 'h', "help", false, "Display command line help and exit.")
|
||||||
|
golf.StringVarP(&flags.logLevel, 'L', "log-level", "info", "Log level to use.")
|
||||||
|
golf.BoolVarP(&flags.quiet, 'q', "quiet", false, "Quiet mode; prevents logging to stderr.")
|
||||||
|
golf.BoolVarP(&flags.logSyslog, 's', "syslog", false, "Log to local syslog.")
|
||||||
|
|
||||||
|
golf.Parse()
|
||||||
|
if help {
|
||||||
|
golf.Usage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// This utility will load its configuration then start listening on
|
||||||
|
// a UNIX socket. It will be handle messages that can :
|
||||||
|
// - stop the program,
|
||||||
|
// - update the configuration,
|
||||||
|
// - check a single entry for replacement,
|
||||||
|
// - check all entries for replacement.
|
||||||
|
// Both check commands include a flag that will force replacement.
|
||||||
|
|
||||||
|
flags := parseCommandLine()
|
||||||
|
err := configureLogging(flags)
|
||||||
|
if err != nil {
|
||||||
|
log.WithField("error", err).Fatal("Failed to configure logging.")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := loadConfiguration(flags.cfgFile)
|
||||||
|
if err != nil {
|
||||||
|
log.WithField("error", err).Fatal("Failed to load initial configuration.")
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := initSocket(cfg.Socket)
|
||||||
|
if err != nil {
|
||||||
|
log.WithField("error", err).Fatal("Failed to initialize socket.")
|
||||||
|
}
|
||||||
|
listener.Close()
|
||||||
|
}
|
47
socket.go
Normal file
47
socket.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func configureSocket(cfg tSocketConfig) error {
|
||||||
|
if cfg.Group != "" {
|
||||||
|
group, err := user.LookupGroup(cfg.Group)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Group %s not found: %w", cfg.Group, err)
|
||||||
|
}
|
||||||
|
gid, err := strconv.Atoi(group.Gid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Group %s has non-numeric GID %s", cfg.Group, group.Gid)
|
||||||
|
}
|
||||||
|
err = os.Chown(cfg.Path, -1, gid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Cannot change group on UNIX socket: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := os.Chmod(cfg.Path, cfg.Mode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Cannot set UNIX socket access mode: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initSocket(cfg tSocketConfig) (net.Listener, error) {
|
||||||
|
listener, err := net.Listen("unix", cfg.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Cannot listen on UNIX socket at %s: %w", cfg.Path, err)
|
||||||
|
}
|
||||||
|
err = configureSocket(cfg)
|
||||||
|
if err != nil {
|
||||||
|
listener.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.WithField("path", cfg.Path).Info("UNIX socket created")
|
||||||
|
return listener, nil
|
||||||
|
}
|
Loading…
Reference in a new issue