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
|
||||
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
|
||||
|
||||
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