18 Commits

Author SHA1 Message Date
cd5f7611a9 notifications: add http service
All checks were successful
ci/woodpecker/push/go-lint Pipeline was successful
2025-03-13 23:35:00 -05:00
b542711078 notifications: add http service 2025-03-13 23:34:37 -05:00
52dbc353e5 change: update go toolchain
All checks were successful
ci/woodpecker/push/go-lint Pipeline was successful
2025-03-12 23:22:52 -05:00
6bef0c3e5b notifications: add http config
All checks were successful
ci/woodpecker/push/go-lint Pipeline was successful
2025-03-12 10:10:45 -05:00
4d705d78fb fix: golang-ci-lint version
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2025-03-11 22:22:39 -05:00
62d47ecfa7 fix: pipeline errors
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/tag/gitea Pipeline was successful
ci/woodpecker/tag/publish-docs Pipeline was successful
ci/woodpecker/release/publish-docs Pipeline was successful
2025-03-11 22:08:59 -05:00
32444ff82e fix: docs and pipeline errors
Some checks failed
ci/woodpecker/tag/gitea Pipeline failed
ci/woodpecker/tag/publish-docs Pipeline was successful
ci/woodpecker/push/go-lint Pipeline failed
ci/woodpecker/push/publish-docs Pipeline failed
2025-03-11 21:55:51 -05:00
a5a7c05640 v0.10.1
Some checks failed
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/tag/gitea Pipeline failed
ci/woodpecker/tag/publish-docs Pipeline was successful
2025-03-11 21:37:58 -05:00
bfb81e11b2 version bump
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
ci/woodpecker/push/publish-docs Pipeline was successful
2025-03-11 21:36:53 -05:00
fd4c83f9c0 Vault: keys are now referenced by name, and the actual data by data
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
ci/woodpecker/push/publish-docs Pipeline failed
2025-03-11 21:33:06 -05:00
fe27c6396a LinuxUserManager: correct parameters for AddUser()
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2025-03-11 16:13:29 -05:00
c89dde186a UserCommands: change field name
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
ci/woodpecker/push/publish-docs Pipeline was successful
2025-03-11 15:37:12 -05:00
18a64de0de UserCommands: change field name 2025-03-11 15:36:43 -05:00
99c622b69f UserCommands: add field CreateUserHome 2025-03-11 15:30:07 -05:00
95e85e8b45 UserCommands: add ssh public keys when running locally
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2025-03-11 15:21:02 -05:00
1a48c7bca5 change: create temp file when modifing password over SSH 2025-03-11 14:55:02 -05:00
5d21764ef1 fix: don't test empty env files
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2025-03-11 13:42:40 -05:00
c7302f0e17 update docs
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
ci/woodpecker/push/publish-docs Pipeline was successful
2025-03-10 12:34:33 -05:00
22 changed files with 304 additions and 114 deletions

View File

@ -0,0 +1,3 @@
kind: Added
body: 'Notifications: http service added'
time: 2025-03-13T23:23:20.130625927-05:00

View File

@ -0,0 +1,3 @@
kind: Changed
body: 'vault: initialize vault before validating config'
time: 2025-03-13T22:48:40.584581357-05:00

8
.changes/v0.10.1.md Normal file
View File

@ -0,0 +1,8 @@
## v0.10.1 - 2025-03-11
### Added
* UserCommands: add ssh public keys when running locally
* UserCommands: add field CreateUserHome
### Changed
* UserCommands: create temp file when modifing password over SSH
* UserCommands: change field name
* Vault: keys are now referenced by `name`, and the actual data by `data`

View File

@ -1,9 +1,7 @@
name: goreleaser release
steps: steps:
golang: golang:
image: golang:1.23 image: golang:1.23
commands: commands:
- go mod tidy
- go install github.com/goreleaser/goreleaser/v2@v2.7.0 - go install github.com/goreleaser/goreleaser/v2@v2.7.0
- goreleaser release -f .goreleaser/gitea.yml --release-notes=".changes/$(go run backy.go version -V).md" - goreleaser release -f .goreleaser/gitea.yml --release-notes=".changes/$(go run backy.go version -V).md"
environment: environment:

View File

@ -5,7 +5,7 @@ steps:
- go build - go build
- go test - go test
release: release:
image: golangci/golangci-lint:v1.53.3 image: golangci/golangci-lint:v1.64.7
commands: commands:
- golangci-lint run -v --timeout 5m - golangci-lint run -v --timeout 5m

View File

@ -6,6 +6,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie). and is generated by [Changie](https://github.com/miniscruff/changie).
## v0.10.1 - 2025-03-11
### Added
* UserCommands: add ssh public keys when running locally
* UserCommands: add field CreateUserHome
### Changed
* UserCommands: create temp file when modifing password over SSH
* UserCommands: change field name
* Vault: keys are now referenced by `name`, and the actual data by `data`
## v0.10.0 - 2025-03-08 ## v0.10.0 - 2025-03-08
### Added ### Added
* Hooks: improved logging when executing * Hooks: improved logging when executing

View File

@ -7,7 +7,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
const versionStr = "0.10.0" const versionStr = "0.10.1"
var ( var (
versionCmd = &cobra.Command{ versionCmd = &cobra.Command{

View File

@ -16,7 +16,7 @@ Values available for this section **(case-sensitive)**:
| ----------------| ------------------------------------------------------------------------------------------------------- | --------------------- | -------- |----------------------------| | ----------------| ------------------------------------------------------------------------------------------------------- | --------------------- | -------- |----------------------------|
| `cmd` | Defines the command to execute | `string` | yes | No | | `cmd` | Defines the command to execute | `string` | yes | No |
| `Args` | Defines the arguments to the command | `[]string` | no | No | | `Args` | Defines the arguments to the command | `[]string` | no | No |
| `environment` | Defines environment variables for the command | `[]string` | no | No | | `environment` | Defines environment variables for the command | `[]string` | no | Partial |
| `type` | See documentation further down the page. Additional fields may be required. | `string` | no | No | | `type` | See documentation further down the page. Additional fields may be required. | `string` | no | No |
| `getOutput` | Command(s) output is in the notification(s) | `bool` | no | No | | `getOutput` | Command(s) output is in the notification(s) | `bool` | no | No |
| `host` | If not specified, the command will execute locally. | `string` | no | No | | `host` | If not specified, the command will execute locally. | `string` | no | No |
@ -95,8 +95,9 @@ The following options are available:
The environment variables support expansion: The environment variables support expansion:
- using escaped values `$VAR` or `${VAR}` - using escaped values `$VAR` or `${VAR}`
- using any external directive, and if using the env directive, the variable will be read from a `.env` file
For now, the variables have to be defined in an `.env` file in the same directory that the program is run from. <!-- For now, the variables expanded have to be defined in an `.env` file in the same directory that the program is run from. -->
If using it with host specified, the SSH server has to be configured to accept those env variables. If using it with host specified, the SSH server has to be configured to accept those env variables.

View File

@ -6,16 +6,18 @@ description: This is dedicated to user commands.
This is dedicated to `user` commands. The command `type` field must be `user`. User is a type that allows one to perform user operations. There are several additional options available when `type` is `user`: This is dedicated to `user` commands. The command `type` field must be `user`. User is a type that allows one to perform user operations. There are several additional options available when `type` is `user`:
| name | notes | type | required | | name | notes | type | required | External directive support
| --- | --- | --- | --- | | ----------------| -------------------------------------------------------------| ---------- | ---------| --------------------------|
| `userName` | The name of a user to be configured. | `string` | yes | | `userName` | The name of a user to be configured. | `string` | yes | no |
| `userOperation` | The type of operation to perform. | `string` | yes | | `userOperation` | The type of operation to perform. | `string` | yes | no |
| `userID` | The user ID to use. | `string` | no | | `userID` | The user ID to use. | `string` | no | no |
| `userGroups` | The groups the user should be added to. | `[]string` | no | | `userGroups` | The groups the user should be added to. | `[]string` | no | no |
| `userSshPubKeys` | The keys to add to the user's authorized keys. | `[]string` | no | | `systemUser` | Create a system user. | `bool` | no | no |
| `userShell` | The shell for the user. | `string` | no | | `userCreateHome`| Create the home directory. | `bool` | no | no |
| `userHome` | The user's home directory. | `string` | no | | `userSshPubKeys`| The keys to add to the user's authorized keys. | `[]string` | no | yes |
| `userPassword` | The new password value when using the `password` operation. Can be specified by using external directive. | `string` | no | | `userShell` | The shell for the user. | `string` | no | no |
| `userHome` | The user's home directory. | `string` | no | no |
| `userPassword` | The new password value when using the `password` operation. | `string` | no | yes |
#### example #### example

View File

@ -6,7 +6,7 @@ description: Set up and configure vault.
[Vault](https://www.vaultproject.io/) is a tool for storing secrets and other data securely. [Vault](https://www.vaultproject.io/) is a tool for storing secrets and other data securely.
Vault config can be used by prefixing `vault:` in front of a password or ENV var. A Vault key can be used by prefixing `%{vault:vault.keys.name}%` in a field that supports external directives.
This is the object in the config file: This is the object in the config file:
@ -18,10 +18,12 @@ vault:
keys: keys:
- name: mongourl - name: mongourl
mountpath: secret mountpath: secret
key: data
path: mongo/url path: mongo/url
type: # KVv1 or KVv2 type: KVv2 # KVv1 or KVv2
- name: - name: someKeyName
path: mountpath: secret
type: key: keyData
mountpath: type: KVv2
path: some/path
``` ```

6
go.mod
View File

@ -2,14 +2,13 @@ module git.andrewnw.xyz/CyberShell/backy
go 1.23 go 1.23
toolchain go1.23.6 toolchain go1.23.7
replace git.andrewnw.xyz/CyberShell/backy => /home/andrew/Projects/backy
require ( require (
github.com/aws/aws-sdk-go-v2/service/s3 v1.76.0 github.com/aws/aws-sdk-go-v2/service/s3 v1.76.0
github.com/dmarkham/enumer v1.5.11 github.com/dmarkham/enumer v1.5.11
github.com/go-co-op/gocron v1.37.0 github.com/go-co-op/gocron v1.37.0
github.com/google/uuid v1.6.0
github.com/hashicorp/vault/api v1.15.0 github.com/hashicorp/vault/api v1.15.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/kevinburke/ssh_config v1.2.0 github.com/kevinburke/ssh_config v1.2.0
@ -51,7 +50,6 @@ require (
github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect

View File

@ -11,6 +11,7 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"strings"
"text/template" "text/template"
"embed" "embed"
@ -95,7 +96,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
command.Shell = "sh" command.Shell = "sh"
} }
localCMD = exec.Command(command.Shell, command.Args...) localCMD = exec.Command(command.Shell, command.Args...)
injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger) injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger, opts)
cmdOutWriters = io.MultiWriter(&cmdOutBuf) cmdOutWriters = io.MultiWriter(&cmdOutBuf)
@ -176,7 +177,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
localCMD.Dir = *command.Dir localCMD.Dir = *command.Dir
} }
injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger) injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger, opts)
cmdOutWriters = io.MultiWriter(&cmdOutBuf) cmdOutWriters = io.MultiWriter(&cmdOutBuf)
@ -194,6 +195,63 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Name, err)).Send() cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Name, err)).Send()
return outputArr, err return outputArr, err
} }
if command.Type == UserCT {
if command.UserOperation == "add" {
if command.UserSshPubKeys != nil {
var (
f *os.File
err error
userHome []byte
)
cmdCtxLogger.Info().Msg("adding SSH Keys")
localCMD := exec.Command(fmt.Sprintf("grep \"%s\" /etc/passwd | cut -d: -f6", command.Username))
userHome, err = localCMD.CombinedOutput()
if err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error finding user home from /etc/passwd: %v", err)
}
command.UserHome = strings.TrimSpace(string(userHome))
userSshDir := fmt.Sprintf("%s/.ssh", command.UserHome)
if _, err := os.Stat(userSshDir); os.IsNotExist(err) {
err := os.MkdirAll(userSshDir, 0700)
if err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error creating directory %s %v", userSshDir, err)
}
}
if _, err := os.Stat(fmt.Sprintf("%s/authorized_keys", userSshDir)); os.IsNotExist(err) {
_, err := os.Create(fmt.Sprintf("%s/authorized_keys", userSshDir))
if err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error creating file %s/authorized_keys: %v", userSshDir, err)
}
}
f, err = os.OpenFile(fmt.Sprintf("%s/authorized_keys", userSshDir), 0700, os.ModeAppend)
if err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error opening file %s/authorized_keys: %v", userSshDir, err)
}
defer f.Close()
for _, k := range command.UserSshPubKeys {
buf := bytes.NewBufferString(k)
cmdCtxLogger.Info().Str("key", k).Msg("adding SSH key")
if _, err := f.ReadFrom(buf); err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error adding to authorized keys: %v", err)
}
}
localCMD = exec.Command(fmt.Sprintf("chown -R %s:%s %s", command.Username, command.Username, userHome))
_, err = localCMD.CombinedOutput()
if err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), err
}
}
}
}
} }
return outputArr, nil return outputArr, nil
} }
@ -405,7 +463,7 @@ func (cmd *Command) ExecuteHooks(hookType string, opts *ConfigOpts) {
cmdLogger := opts.Logger.With(). cmdLogger := opts.Logger.With().
Str("backy-cmd", v).Str("hookType", "error"). Str("backy-cmd", v).Str("hookType", "error").
Logger() Logger()
errCmd.RunCmd(cmdLogger, opts) _, _ = errCmd.RunCmd(cmdLogger, opts)
} }
case "success": case "success":
@ -415,7 +473,7 @@ func (cmd *Command) ExecuteHooks(hookType string, opts *ConfigOpts) {
cmdLogger := opts.Logger.With(). cmdLogger := opts.Logger.With().
Str("backy-cmd", v).Str("hookType", "success"). Str("backy-cmd", v).Str("hookType", "success").
Logger() Logger()
successCmd.RunCmd(cmdLogger, opts) _, _ = successCmd.RunCmd(cmdLogger, opts)
} }
case "final": case "final":
for _, v := range cmd.Hooks.Final { for _, v := range cmd.Hooks.Final {
@ -424,7 +482,7 @@ func (cmd *Command) ExecuteHooks(hookType string, opts *ConfigOpts) {
cmdLogger := opts.Logger.With(). cmdLogger := opts.Logger.With().
Str("backy-cmd", v).Str("hookType", "final"). Str("backy-cmd", v).Str("hookType", "final").
Logger() Logger()
finalCmd.RunCmd(cmdLogger, opts) _, _ = finalCmd.RunCmd(cmdLogger, opts)
} }
} }
} }

View File

@ -1,7 +1,6 @@
package backy package backy
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
@ -126,6 +125,10 @@ func (opts *ConfigOpts) ReadConfig() *ConfigOpts {
log.Info().Str("config file", opts.ConfigFilePath).Send() log.Info().Str("config file", opts.ConfigFilePath).Send()
if err := opts.setupVault(); err != nil {
log.Err(err).Send()
}
unmarshalConfig(backyKoanf, "commands", &opts.Cmds, opts.Logger) unmarshalConfig(backyKoanf, "commands", &opts.Cmds, opts.Logger)
getCommandEnvironments(opts) getCommandEnvironments(opts)
@ -154,10 +157,6 @@ func (opts *ConfigOpts) ReadConfig() *ConfigOpts {
opts.SetupNotify() opts.SetupNotify()
if err := opts.setupVault(); err != nil {
log.Err(err).Send()
}
return opts return opts
} }
@ -221,6 +220,7 @@ func setLoggingOptions(k *koanf.Koanf, opts *ConfigOpts) {
logFile = k.String(getLoggingKeyFromConfig("file")) logFile = k.String(getLoggingKeyFromConfig("file"))
opts.LogFilePath = logFile opts.LogFilePath = logFile
} }
opts.LogFilePath = logFile
zerolog.SetGlobalLevel(zerolog.InfoLevel) zerolog.SetGlobalLevel(zerolog.InfoLevel)
if isLoggingVerbose { if isLoggingVerbose {
@ -248,6 +248,9 @@ func unmarshalConfig(k *koanf.Koanf, key string, target interface{}, log zerolog
func getCommandEnvironments(opts *ConfigOpts) { func getCommandEnvironments(opts *ConfigOpts) {
for cmdName, cmdConf := range opts.Cmds { for cmdName, cmdConf := range opts.Cmds {
if cmdConf.Env == "" {
continue
}
opts.Logger.Debug().Str("env file", cmdConf.Env).Str("cmd", cmdName).Send() opts.Logger.Debug().Str("env file", cmdConf.Env).Str("cmd", cmdName).Send()
if err := testFile(cmdConf.Env); err != nil { if err := testFile(cmdConf.Env); err != nil {
logging.ExitWithMSG("Could not open file"+cmdConf.Env+": "+err.Error(), 1, &opts.Logger) logging.ExitWithMSG("Could not open file"+cmdConf.Env+": "+err.Error(), 1, &opts.Logger)
@ -472,62 +475,6 @@ func (opts *ConfigOpts) setupVault() error {
return nil return nil
} }
func getVaultSecret(vaultClient *vault.Client, key *VaultKey) (string, error) {
var (
secret *vault.KVSecret
err error
)
if key.ValueType == "KVv2" {
secret, err = vaultClient.KVv2(key.MountPath).Get(context.Background(), key.Path)
} else if key.ValueType == "KVv1" {
secret, err = vaultClient.KVv1(key.MountPath).Get(context.Background(), key.Path)
} else if key.ValueType != "" {
return "", fmt.Errorf("type %s for key %s not known. Valid types are KVv1 or KVv2", key.ValueType, key.Name)
} else {
return "", fmt.Errorf("type for key %s must be specified. Valid types are KVv1 or KVv2", key.Name)
}
if err != nil {
return "", fmt.Errorf("unable to read secret: %v", err)
}
value, ok := secret.Data[key.Name].(string)
if !ok {
return "", fmt.Errorf("value type assertion failed: %T %#v", secret.Data[key.Name], secret.Data[key.Name])
}
return value, nil
}
func parseVaultKey(keyName string, keys []*VaultKey) (*VaultKey, error) {
for _, k := range keys {
if k.Name == keyName {
return k, nil
}
}
return nil, fmt.Errorf("key %s not found in vault keys", keyName)
}
func GetVaultKey(str string, opts *ConfigOpts, log zerolog.Logger) string {
key, err := parseVaultKey(str, opts.VaultKeys)
if key == nil && err == nil {
return str
}
if err != nil && key == nil {
log.Err(err).Send()
return ""
}
value, secretErr := getVaultSecret(opts.vaultClient, key)
if secretErr != nil {
log.Err(secretErr).Send()
return value
}
return value
}
func processCmds(opts *ConfigOpts) error { func processCmds(opts *ConfigOpts) error {
// process commands // process commands

View File

@ -9,6 +9,7 @@ import (
"git.andrewnw.xyz/CyberShell/backy/pkg/logging" "git.andrewnw.xyz/CyberShell/backy/pkg/logging"
"github.com/nikoksr/notify" "github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/http"
"github.com/nikoksr/notify/service/mail" "github.com/nikoksr/notify/service/mail"
"github.com/nikoksr/notify/service/matrix" "github.com/nikoksr/notify/service/matrix"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
@ -30,6 +31,12 @@ type MailConfig struct {
Password string `yaml:"password"` Password string `yaml:"password"`
} }
type HttpConfig struct {
URL string `yaml:"url"`
Method string `yaml:"method"`
Headers map[string][]string `yaml:"headers"`
}
// SetupNotify sets up notify instances for each command list. // SetupNotify sets up notify instances for each command list.
func (opts *ConfigOpts) SetupNotify() { func (opts *ConfigOpts) SetupNotify() {
@ -59,6 +66,7 @@ func (opts *ConfigOpts) SetupNotify() {
continue continue
} }
conf.Password = getExternalConfigDirectiveValue(conf.Password, opts) conf.Password = getExternalConfigDirectiveValue(conf.Password, opts)
opts.Logger.Debug().Str("list", confName).Str("id", confId).Msg("adding mail notification service")
mailConf := setupMail(conf) mailConf := setupMail(conf)
services = append(services, mailConf) services = append(services, mailConf)
case "matrix": case "matrix":
@ -68,13 +76,22 @@ func (opts *ConfigOpts) SetupNotify() {
continue continue
} }
conf.AccessToken = getExternalConfigDirectiveValue(conf.AccessToken, opts) conf.AccessToken = getExternalConfigDirectiveValue(conf.AccessToken, opts)
opts.Logger.Debug().Str("list", confName).Str("id", confId).Msg("adding matrix notification service")
mtrxConf, mtrxErr := setupMatrix(conf) mtrxConf, mtrxErr := setupMatrix(conf)
if mtrxErr != nil { if mtrxErr != nil {
opts.Logger.Info().Str("list", confName).Err(fmt.Errorf("error: configuring matrix id %s failed during setup: %w", id, mtrxErr)) opts.Logger.Info().Str("list", confName).Err(fmt.Errorf("error: configuring matrix id %s failed during setup: %w", id, mtrxErr))
continue continue
} }
services = append(services, mtrxConf) services = append(services, mtrxConf)
case "http":
conf, ok := opts.NotificationConf.HttpConfig[confId]
if !ok {
opts.Logger.Info().Err(fmt.Errorf("error: ID %s not found in http object", confId)).Str("list", confName).Send()
continue
}
opts.Logger.Debug().Str("list", confName).Str("id", confId).Msg("adding http notification service")
httpConf := setupHttp(conf)
services = append(services, httpConf)
default: default:
opts.Logger.Info().Err(fmt.Errorf("id %s not found", id)).Str("list", confName).Send() opts.Logger.Info().Err(fmt.Errorf("id %s not found", id)).Str("list", confName).Send()
} }
@ -100,3 +117,19 @@ func setupMail(config MailConfig) *mail.Mail {
mailClient.BodyFormat(mail.PlainText) mailClient.BodyFormat(mail.PlainText)
return mailClient return mailClient
} }
func setupHttp(httpConf HttpConfig) *http.Service {
httpService := http.New()
httpService.AddReceivers(&http.Webhook{
URL: httpConf.URL,
Header: httpConf.Headers,
ContentType: "text/plain",
Method: httpConf.Method,
BuildPayload: func(subject, message string) (payload any) {
return subject + "\n\n" + message
},
})
return httpService
}

View File

@ -15,6 +15,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/google/uuid"
"github.com/kevinburke/ssh_config" "github.com/kevinburke/ssh_config"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/pkg/sftp" "github.com/pkg/sftp"
@ -509,9 +510,32 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
if command.Type == UserCT && command.UserOperation == "password" { if command.Type == UserCT && command.UserOperation == "password" {
// cmdCtxLogger.Debug().Msgf("adding stdin") // cmdCtxLogger.Debug().Msgf("adding stdin")
userNamePass := fmt.Sprintf("%s:%s", command.Username, command.UserPassword)
ArgsStr = fmt.Sprintf("echo %s | chpasswd", userNamePass)
userNamePass := fmt.Sprintf("%s:%s", command.Username, command.UserPassword)
client, err := sftp.NewClient(command.RemoteHost.SshClient)
if err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error creating sftp client: %v", err)
}
uuidFile := uuid.New()
passFilePath := fmt.Sprintf("/tmp/%s", uuidFile.String())
passFile, passFileErr := client.Create(passFilePath)
if passFileErr != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error creating file /tmp/%s: %v", uuidFile.String(), passFileErr)
}
_, err = passFile.Write([]byte(userNamePass))
if err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error writing to file /tmp/%s: %v", uuidFile.String(), err)
}
ArgsStr = fmt.Sprintf("cat %s | chpasswd", passFilePath)
defer passFile.Close()
rmFileFunc := func() {
_ = client.Remove(passFilePath)
}
defer rmFileFunc()
// commandSession.Stdin = command.stdin // commandSession.Stdin = command.stdin
} }
if err := commandSession.Run(ArgsStr); err != nil { if err := commandSession.Run(ArgsStr); err != nil {
@ -544,7 +568,10 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error creating sftp client: %v", err) return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error creating sftp client: %v", err)
} }
client.MkdirAll(userSshDir) err = client.MkdirAll(userSshDir)
if err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error creating directory %s: %v", userSshDir, err)
}
_, err = client.Create(fmt.Sprintf("%s/authorized_keys", userSshDir)) _, err = client.Create(fmt.Sprintf("%s/authorized_keys", userSshDir))
if err != nil { if err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error opening file %s/authorized_keys: %v", userSshDir, err) return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error opening file %s/authorized_keys: %v", userSshDir, err)

View File

@ -115,7 +115,9 @@ type (
UserShell string `yaml:"userShell,omitempty"` UserShell string `yaml:"userShell,omitempty"`
SystemUser bool `yaml:"systemUser,omitempty"` UserCreateHome bool `yaml:"userCreateHome,omitempty"`
UserIsSystem bool `yaml:"userIsSystem,omitempty"`
UserPassword string `yaml:"userPassword,omitempty"` UserPassword string `yaml:"userPassword,omitempty"`
@ -221,6 +223,7 @@ type (
VaultKey struct { VaultKey struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Key string `yaml:"key"`
Path string `yaml:"path"` Path string `yaml:"path"`
ValueType string `yaml:"type"` ValueType string `yaml:"type"`
MountPath string `yaml:"mountpath"` MountPath string `yaml:"mountpath"`
@ -236,6 +239,7 @@ type (
Notifications struct { Notifications struct {
MailConfig map[string]MailConfig `yaml:"mail,omitempty"` MailConfig map[string]MailConfig `yaml:"mail,omitempty"`
MatrixConfig map[string]MatrixStruct `yaml:"matrix,omitempty"` MatrixConfig map[string]MatrixStruct `yaml:"matrix,omitempty"`
HttpConfig map[string]HttpConfig `yaml:"http,omitempty"`
} }
CmdOutput struct { CmdOutput struct {

View File

@ -6,6 +6,7 @@ package backy
import ( import (
"bytes" "bytes"
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -16,6 +17,7 @@ import (
"git.andrewnw.xyz/CyberShell/backy/pkg/logging" "git.andrewnw.xyz/CyberShell/backy/pkg/logging"
"git.andrewnw.xyz/CyberShell/backy/pkg/remotefetcher" "git.andrewnw.xyz/CyberShell/backy/pkg/remotefetcher"
vault "github.com/hashicorp/vault/api"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/knadh/koanf/v2" "github.com/knadh/koanf/v2"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -108,7 +110,11 @@ func injectEnvIntoSSH(envVarsToInject environmentVars, process *ssh.Session, opt
goto errEnvFile goto errEnvFile
} }
for key, val := range envMap { for key, val := range envMap {
process.Setenv(key, GetVaultKey(val, opts, log)) err = process.Setenv(key, GetVaultKey(val, opts, log))
if err != nil {
log.Error().Err(err).Send()
}
} }
} }
@ -119,12 +125,16 @@ errEnvFile:
if strings.Contains(envVal, "=") { if strings.Contains(envVal, "=") {
envVarArr := strings.Split(envVal, "=") envVarArr := strings.Split(envVal, "=")
process.Setenv(envVarArr[0], GetVaultKey(envVarArr[1], opts, log)) err := process.Setenv(envVarArr[0], getExternalConfigDirectiveValue(envVarArr[1], opts))
if err != nil {
log.Error().Err(err).Send()
}
} }
} }
} }
func injectEnvIntoLocalCMD(envVarsToInject environmentVars, process *exec.Cmd, log zerolog.Logger) { func injectEnvIntoLocalCMD(envVarsToInject environmentVars, process *exec.Cmd, log zerolog.Logger, opts *ConfigOpts) {
if envVarsToInject.file != "" { if envVarsToInject.file != "" {
envPath, _ := getFullPathWithHomeDir(envVarsToInject.file) envPath, _ := getFullPathWithHomeDir(envVarsToInject.file)
@ -148,7 +158,8 @@ errEnvFile:
for _, envVal := range envVarsToInject.env { for _, envVal := range envVarsToInject.env {
if strings.Contains(envVal, "=") { if strings.Contains(envVal, "=") {
process.Env = append(process.Env, envVal) envVarArr := strings.Split(envVal, "=")
process.Env = append(process.Env, fmt.Sprintf("%s=%s", envVarArr[0], getExternalConfigDirectiveValue(envVarArr[1], opts)))
} }
} }
process.Env = append(process.Env, os.Environ()...) process.Env = append(process.Env, os.Environ()...)
@ -249,7 +260,6 @@ func (opts *ConfigOpts) loadEnv() {
func expandEnvVars(backyEnv map[string]string, envVars []string) { func expandEnvVars(backyEnv map[string]string, envVars []string) {
env := func(name string) string { env := func(name string) string {
name = strings.ToUpper(name)
envVar, found := backyEnv[name] envVar, found := backyEnv[name]
if found { if found {
return envVar return envVar
@ -258,14 +268,14 @@ func expandEnvVars(backyEnv map[string]string, envVars []string) {
} }
for indx, v := range envVars { for indx, v := range envVars {
if strings.HasPrefix(v, externDirectiveStart) && strings.HasSuffix(v, externDirectiveEnd) {
if strings.HasPrefix(v, envExternDirectiveStart) { if strings.HasPrefix(v, envExternDirectiveStart) && strings.HasSuffix(v, externDirectiveEnd) {
v = strings.TrimPrefix(v, envExternDirectiveStart) v = strings.TrimPrefix(v, envExternDirectiveStart)
v = strings.TrimRight(v, externDirectiveEnd) v = strings.TrimRight(v, externDirectiveEnd)
out, _ := shell.Expand(v, env) out, _ := shell.Expand(v, env)
envVars[indx] = out envVars[indx] = out
}
} }
} }
} }
@ -293,7 +303,8 @@ func getCommandTypeAndSetCommandInfo(command *Command) *Command {
command.Username, command.Username,
command.UserHome, command.UserHome,
command.UserShell, command.UserShell,
command.SystemUser, command.UserIsSystem,
command.UserCreateHome,
command.UserGroups, command.UserGroups,
command.Args) command.Args)
case "modify": case "modify":
@ -385,3 +396,57 @@ func getExternalConfigDirectiveValue(key string, opts *ConfigOpts) string {
return key return key
} }
func getVaultSecret(vaultClient *vault.Client, key *VaultKey) (string, error) {
var (
secret *vault.KVSecret
err error
)
if key.ValueType == "KVv2" {
secret, err = vaultClient.KVv2(key.MountPath).Get(context.Background(), key.Path)
} else if key.ValueType == "KVv1" {
secret, err = vaultClient.KVv1(key.MountPath).Get(context.Background(), key.Path)
} else if key.ValueType != "" {
return "", fmt.Errorf("type %s for key %s not known. Valid types are KVv1 or KVv2", key.ValueType, key.Name)
} else {
return "", fmt.Errorf("type for key %s must be specified. Valid types are KVv1 or KVv2", key.Name)
}
if err != nil {
return "", fmt.Errorf("unable to read secret: %v", err)
}
value, ok := secret.Data[key.Key].(string)
if !ok {
return "", fmt.Errorf("value type assertion failed for vault key %s: %T %#v", key.Name, secret.Data[key.Name], secret.Data[key.Name])
}
return value, nil
}
func getVaultKeyData(keyName string, keys []*VaultKey) (*VaultKey, error) {
for _, k := range keys {
if k.Name == keyName {
return k, nil
}
}
return nil, fmt.Errorf("key %s not found in vault keys", keyName)
}
func GetVaultKey(str string, opts *ConfigOpts, log zerolog.Logger) string {
key, err := getVaultKeyData(str, opts.VaultKeys)
if key == nil && err == nil {
return str
}
if err != nil && key == nil {
log.Err(err).Send()
return ""
}
value, secretErr := getVaultSecret(opts.vaultClient, key)
if secretErr != nil {
log.Err(secretErr).Send()
return value
}
return value
}

View File

@ -116,7 +116,7 @@ func (c *Cache) Set(source, hash string, data []byte, dataType string) (CacheDat
path := filepath.Join(c.dir, fmt.Sprintf("%s-%s", fileName, sourceHash)) path := filepath.Join(c.dir, fmt.Sprintf("%s-%s", fileName, sourceHash))
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
os.MkdirAll(c.dir, 0700) _ = os.MkdirAll(c.dir, 0700)
} }
err := os.WriteFile(path, data, 0644) err := os.WriteFile(path, data, 0644)
@ -171,7 +171,7 @@ func (cf *CachedFetcher) Hash(data []byte) string {
func LoadMetadataFromFile(filePath string) ([]*CacheData, error) { func LoadMetadataFromFile(filePath string) ([]*CacheData, error) {
if _, err := os.Stat(filePath); os.IsNotExist(err) { if _, err := os.Stat(filePath); os.IsNotExist(err) {
// Create the file if it does not exist // Create the file if it does not exist
os.MkdirAll(path.Dir(filePath), 0700) _ = os.MkdirAll(path.Dir(filePath), 0700)
emptyData := []byte("[]") emptyData := []byte("[]")
err := os.WriteFile(filePath, emptyData, 0644) err := os.WriteFile(filePath, emptyData, 0644)
if err != nil { if err != nil {

View File

@ -15,7 +15,7 @@ func (l LinuxUserManager) NewLinuxManager() *LinuxUserManager {
} }
// AddUser adds a new user to the system. // AddUser adds a new user to the system.
func (l LinuxUserManager) AddUser(username, homeDir, shell string, isSystem bool, groups, args []string) (string, []string) { func (l LinuxUserManager) AddUser(username, homeDir, shell string, isSystem, createHome bool, groups, args []string) (string, []string) {
baseArgs := []string{} baseArgs := []string{}
if isSystem { if isSystem {
@ -38,6 +38,10 @@ func (l LinuxUserManager) AddUser(username, homeDir, shell string, isSystem bool
baseArgs = append(baseArgs, args...) baseArgs = append(baseArgs, args...)
} }
if createHome {
baseArgs = append(baseArgs, "-m")
}
args = append(baseArgs, username) args = append(baseArgs, username)
cmd := "useradd" cmd := "useradd"

View File

@ -10,7 +10,7 @@ import (
// UserManager defines the interface for user management operations. // UserManager defines the interface for user management operations.
// All functions but one return a string for the command and any args. // All functions but one return a string for the command and any args.
type UserManager interface { type UserManager interface {
AddUser(username, homeDir, shell string, isSystem bool, groups, args []string) (string, []string) AddUser(username, homeDir, shell string, createHome, isSystem bool, groups, args []string) (string, []string)
RemoveUser(username string) (string, []string) RemoveUser(username string) (string, []string)
ModifyUser(username, homeDir, shell string, groups []string) (string, []string) ModifyUser(username, homeDir, shell string, groups []string) (string, []string)
// Modify password uses chpasswd for Linux systems to build the command to change the password // Modify password uses chpasswd for Linux systems to build the command to change the password

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -eou pipefail set -eou pipefail
go mod tidy
go generate ./... go generate ./...
CURRENT_TAG="$(go run backy.go version -V)" CURRENT_TAG="$(go run backy.go version -V)"
goreleaser -f .goreleaser/github.yml check goreleaser -f .goreleaser/github.yml check

27
tests/VaultTest.yml Normal file
View File

@ -0,0 +1,27 @@
commands:
vaultEnvVar:
cmd: echo
shell: /bin/zsh
Args:
- ${VAULT_VAR}
environment:
"VAULT_VAR=%{vault:vaultTestSecret}%"
logging:
verbose: true
vault:
token: root
address: http://127.0.0.1:8200
enabled: true
keys:
- name: vaultTestSecret
key: data
mountpath: secret
path: test/var
type: KVv2 # KVv1 or KVv2
cmdLists:
addUsers:
order:
- vaultEnvVar