Compare commits

..

14 commits

11 changed files with 489 additions and 15 deletions

View file

@ -11,7 +11,9 @@
{;; Application
org.clojure/clojure {:mvn/version "1.11.4"}
org.clojure/spec.alpha {:mvn/version "0.5.238"}
orchestra/orchestra {:mvn/version "2021.01.01-1"}}
orchestra/orchestra {:mvn/version "2021.01.01-1"}
cheshire/cheshire {:mvn/version "5.13.0"}
com.widdindustries/cljc.java-time {:mvn/version "0.1.21"}}
;; ---------------------------------------------------------
;; ---------------------------------------------------------

View file

@ -1,5 +1,30 @@
# Credential Rotation
## change password step
```mermaid
stateDiagram-v2
noAction: no-pwd-change-needed
wait: wait-for-new-pwd
new: change-pwd
finished: pwd-change-finished
state configExist? <<choice>>
state valid? <<choice>>
state finished? <<choice>>
[*] --> configExist?
configExist? --> valid?: new-password-config-exist?
configExist? --> noAction
valid? --> finished?: valid-from > now?
valid? --> wait
finished? --> finished: current > valid-from?
finished? --> new
new --> [*]
finished --> [*]
noAction --> [*]
wait --> [*]
```
## Example Data
Default
@ -86,7 +111,7 @@ Validation:
Steps to perform:
- Add new password
- `restic -r <repo> key add --new-password-file <file>`
- `restic -r <repo> --new-password-file <file> key passwd`
#### New password has been added

View file

@ -5,3 +5,6 @@ ADD resources /tmp/
RUN /tmp/install.sh
ADD local/ /usr/local/lib/dda-backup
RUN init-bb.bb
#ADD resources2 /tmp/
#RUN install -m 0700 -o root -g root /tmp/test.bb /usr/local/bin/
#RUN test.bb

View file

@ -18,12 +18,12 @@ function main() {
apt-get install -qqy ca-certificates curl gnupg postgresql-client-16 restic openjdk-21-jre-headless nano
curl -Ss --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/postgresql-common_pgdg_archive_keyring.gpg
sh -c 'echo "deb [signed-by=/etc/apt/trusted.gpg.d/postgresql-common_pgdg_archive_keyring.gpg] https://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
upgradeSystem
babashka_install
} > /dev/null
update-ca-certificates
install -m 0700 -o root -g root /tmp/init-bb.bb /usr/local/bin/
install -m 0600 -o root -g root /tmp/bb.edn /usr/local/bin/
cleanupDocker
}

View file

@ -0,0 +1,33 @@
#!/usr/bin/env bb
(require '[dda.backup.cred-rot :as cr])
(def restic-repo {:password-file "/restic-pwd"
:restic-repository "/restic-repo"})
(def file-config (merge restic-repo {:backup-path "files"
:files ["/test-backup"]
:restore-target-directory "/test-restore"}))
(def cred-config (merge file-config
{:restic-repository "/restic-repo/files"
:new-password-config {:new-password-file "/new-pw"
:valid-from "2024-12-12 00:00:00"}}))
(def dry-run {:dry-run true :debug true})
(defn prepare!
[]
(spit "/restic-pwd" "ThePassword")
(spit "/new-pw" "newPassword"))
(defn change-password!
[]
(println "change-password!")
(cr/change-password! cred-config))
(prepare!)
(change-password!)

View file

@ -0,0 +1,87 @@
#!/usr/bin/env bb
(require '[babashka.tasks :as tasks]
'[dda.backup.core :as bc]
'[dda.backup.cred-rot :as cr]
'[dda.backup.restic :as rc]
'[dda.backup.postgresql :as pg]
'[dda.backup.backup :as bak]
'[dda.backup.restore :as rs])
(def restic-repo {:password-file "restic-pwd"
:restic-repository "/restic-repo"})
(def file-config (merge restic-repo {:backup-path "files"
:files ["/test-backup"]
:restore-target-directory "/test-restore"}))
(def db-config (merge restic-repo {:backup-path "db"
:pg-db "mydb"
:pg-user "user"
:pg-password "password"}))
(def cred-config (merge file-config
{:restic-repository "/restic-repo/files"
:new-password-config {:new-password-file "new-pw"
:valid-from "2024-12-12 00:00:00"}}))
(def dry-run {:dry-run true :debug true})
(defn prepare!
[]
(spit "/tmp/file_password" "file-password")
(spit "/restic-pwd" "ThePassword")
(spit "/new-pw" "newPassword")
(tasks/shell "mkdir" "-p" "/test-backup")
(spit "/test-backup/file" "I was here")
(tasks/shell "mkdir" "-p" "/test-restore")
(pg/create-pg-pass! db-config))
(defn check-env-or-file
[]
(println "check-env-or-file")
(println (bc/env-or-file "FILE_PASSWORD"))
(println (bc/env-or-file "ENV_PASSWORD")))
(defn restic-repo-init!
[]
(println "restic-repo-init!")
(rc/init! file-config)
(rc/init! (merge db-config dry-run)))
(defn restic-backup!
[]
(println "restic-backup!")
(bak/backup-file! file-config)
(bak/backup-db! (merge db-config dry-run)))
(defn list-snapshots!
[]
(println "list-snapshots!")
(rc/list-snapshots! file-config)
(rc/list-snapshots! (merge db-config dry-run)))
(defn restic-restore!
[]
(println "restic-restore!")
(rs/restore-file! file-config)
(pg/drop-create-db! (merge db-config dry-run))
(rs/restore-db! (merge db-config dry-run)))
(defn change-password!
[]
(println "change-password!")
(cr/change-password! cred-config))
(prepare!)
(restic-repo-init!)
(restic-backup!)
(list-snapshots!)
(restic-restore!)
(change-password!)

View file

@ -2,17 +2,18 @@
(require '[babashka.tasks :as tasks]
'[dda.backup.core :as bc]
'[dda.backup.cred-rot :as cr]
'[dda.backup.restic :as rc]
'[dda.backup.postgresql :as pg]
'[dda.backup.backup :as bak]
'[dda.backup.restore :as rs])
(def restic-repo {:password-file "restic-pwd"
:restic-repository "restic-repo"})
:restic-repository "/restic-repo"})
(def file-config (merge restic-repo {:backup-path "files"
:files ["test-backup"]
:restore-target-directory "test-restore"}))
:files ["/test-backup"]
:restore-target-directory "/test-restore"}))
(def db-config (merge restic-repo {:backup-path "db"
@ -20,43 +21,68 @@
:pg-user "user"
:pg-password "password"}))
(def cred-config (merge file-config
{:restic-repository "/restic-repo/files"
:new-password-config {:new-password-file "new-pw"
:valid-from "2024-12-12 00:00:00"}}))
(def dry-run {:dry-run true :debug true})
(defn prepare!
[]
(spit "/tmp/file_password" "file-password")
(println (bc/env-or-file "FILE_PASSWORD"))
(println (bc/env-or-file "ENV_PASSWORD"))
(spit "restic-pwd" "ThePassword")
(tasks/shell "mkdir" "-p" "test-backup")
(spit "test-backup/file" "I was here")
(tasks/shell "mkdir" "-p" "test-restore")
(spit "/restic-pwd" "ThePassword")
(spit "/new-pw" "newPassword")
(tasks/shell "mkdir" "-p" "/test-backup")
(spit "/test-backup/file" "I was here")
(tasks/shell "mkdir" "-p" "/test-restore")
(pg/create-pg-pass! db-config))
(defn check-env-or-file
[]
(println "check-env-or-file")
(println (bc/env-or-file "FILE_PASSWORD"))
(println (bc/env-or-file "ENV_PASSWORD")))
(defn restic-repo-init!
[]
[]
(println "restic-repo-init!")
(rc/init! file-config)
(rc/init! (merge db-config dry-run)))
(defn restic-backup!
[]
[]
(println "restic-backup!")
(bak/backup-file! file-config)
(bak/backup-db! (merge db-config dry-run)))
(defn list-snapshots!
[]
(println "list-snapshots!")
(rc/list-snapshots! file-config)
(rc/list-snapshots! (merge db-config dry-run)))
(defn restic-restore!
[]
[]
(println "restic-restore!")
(rs/restore-file! file-config)
(pg/drop-create-db! (merge db-config dry-run))
(rs/restore-db! (merge db-config dry-run)))
(defn change-password!
[]
(println "change-password!")
(cr/change-password! cred-config))
(prepare!)
(check-env-or-file)
(restic-repo-init!)
(restic-backup!)
(list-snapshots!)
(restic-restore!)
(change-password!)

View file

@ -0,0 +1,72 @@
(ns dda.backup.cred-rot
(:require
[orchestra.core :refer [defn-spec]]
[clojure.spec.alpha :as s]
[dda.backup.cred-rot.domain :as domain]
[dda.backup.infrastructure :as i]
[cljc.java-time.instant :as inst]
[cljc.java-time.local-date :as ld]))
(s/def ::replace-until domain/timestamp?)
(s/def ::new-password-file string?)
(s/def ::new-password-config (s/keys :req-un [::new-password-file ::replace-until]))
(s/def ::cred-rot
(s/keys :req-un []
:opt-un [::new-password-config]))
; Refer to "docs/CredentialRotation.md" for specifics
(defn-spec list-passwords! string?
[config ::cred-rot]
(i/execute-out! (domain/list-passwords-command config) config))
(defn-spec change-password-step! ::domain/set-password-action
[config ::cred-rot]
(when-some [new-password-config (:new-password-config config)]
(let [initial-passwords-list (domain/parse-response (list-passwords! config))
action (domain/set-new-password-action
(ld/now)
initial-passwords-list
config)]
(cond
(= action :wait-for-new-pwd)
(println "nothing to do.")
(= action :change-pwd)
(i/execute! (domain/change-password-command config) config)
(= action :new-pwd-change-finished)
(println "pw-change sucessfull")
(= action :no-pwd-change-needed)
(println "nothing to do.")
:else
(throw (Exception. (str action)))
)
(println initial-passwords-list)
(println action)
(println (list-passwords! config))
action
)))
(defn-spec change-password! nil?
[config ::cred-rot]
(while (let [action (change-password-step! config) ]
(and (not= :no-pwd-change-needed action)
(not= :wait-for-new-pwd action)))
(println "call step")
))
(defn-spec set-new-password-old! nil?
[config ::cred-rot]
(let [{:keys [new-password-file replace-until]} (:new-password-config config)
replace-until-date (ld/parse "2024-10-19 13:16:54" domain/timestamp-formatter)]
(if (not (nil? new-password-file))
(let [parsed-passwords (domain/parse-response (list-passwords! config))]
(println (str "add-new-password: relace until " replace-until))
(cond
(> 0 (compare
(:created (last parsed-passwords))
replace-until-date))
(do (println "add-new-password: set new pw")
(i/execute! (domain/add-password-command config) config))
:else (println (str "else " (inst/now)))))
(println "add-new-password: there was no new pw configured"))))

View file

@ -0,0 +1,111 @@
(ns dda.backup.cred-rot.domain
(:require
[orchestra.core :refer [defn-spec]]
[clojure.spec.alpha :as s]
[clojure.string :as str]
[dda.backup.core.domain :as cd]
[dda.backup.restic.domain :as rd]
[cheshire.core :as cc]
[cljc.java-time.local-date :as ld]
[cljc.java-time.format.date-time-formatter :as df]))
(s/def ::new-password-file string?)
(s/def ::config (s/keys :req-un [::rd/restic-repository
::rd/password-file]
:opt-un [::rd/certificate-file
::new-password-file]))
(def lowercase-numeric #"[a-z0-9]+")
(def alphanumeric #"[a-zA-Z0-9]+")
; true | false
(s/def ::current boolean?)
; 521e0760
(s/def ::id (s/and string? #(re-matches lowercase-numeric %)))
; root
(s/def ::userName #(re-matches alphanumeric %))
; backup-restore-65bd9b6ff5-z69sn
(s/def ::hostName (fn [in] (every? #(re-matches lowercase-numeric %) (str/split in #"-"))))
; "2024-10-18 13:08:16"
(def timestamp-formatter (df/of-pattern "yyyy-MM-dd HH:mm:ss"))
(defn timestamp-string? [in]
(try
(ld/parse in timestamp-formatter)
true
(catch Exception _ false)))
(def timestamp? any?)
(s/def ::created timestamp?)
(s/def ::entry (s/keys :opt-un []
:req-un [::current ::id ::userName ::hostName ::created]))
(s/def ::response (s/coll-of ::entry))
(s/def ::set-password-action #{:error-parse-password :error-undefined
:wait-for-new-pwd :change-pwd :pwd-change-finished :no-pwd-change-needed})
(s/def ::valid-from timestamp-string?)
(s/def ::new-password-file string?)
(s/def ::new-password-config (s/keys :req-un [::new-password-file ::valid-from]))
(s/def ::cred-rot
(s/keys :req-un []
:opt-un [::new-password-config]))
; Refer to "docs/CredentialRotation.md" for specifics
(defn-spec base-command ::cd/command
[config ::config
command ::cd/command]
(let [{:keys [restic-repository password-file
certificate-file new-password-file]} config]
(into
[]
(concat ["restic" "-r" restic-repository]
(cond
(some? certificate-file)
["--cacert" certificate-file]
(some? password-file)
["--password-file" password-file]
:else
[])
command))))
(defn-spec list-passwords-command ::cd/command
[config ::config]
(base-command config ["key" "list" "--json"]))
(defn-spec change-password-command ::cd/command
[config ::config]
(let [{:keys [new-password-file]} (:new-password-config config)]
[(base-command config ["--new-password-file" new-password-file "key" "passwd"])]))
(defn-spec parse-response ::response
[response string?]
(->> (cc/parse-string response true)
(map #(merge % {:created (ld/parse (:created %) timestamp-formatter)}))
(sort-by :created)
))
(defn-spec set-new-password-action ::set-password-action
[current-date timestamp?
parsed-response ::response
config ::cred-rot]
(if-let [new-password-config (:new-password-config config)]
(let [valid-from (:valid-from new-password-config)
valid-from-date (ld/parse valid-from timestamp-formatter)]
(cond
(> 1 (count parsed-response))
:error-parse-password
(> 0 (compare current-date valid-from-date))
:wait-for-new-pwd
(and (<= 0 (compare current-date valid-from-date))
(= 1 (count parsed-response))
(> 0 (compare (:created (last parsed-response)) valid-from-date)))
:change-pwd
(and (<= 0 (compare current-date valid-from-date))
(= 1 (count parsed-response))
(<= 0 (compare (:created (last parsed-response)) valid-from-date))
(:current (last parsed-response)))
:pwd-change-finished
:else
:error-undefined))
:no-pwd-change-needed))

View file

@ -12,3 +12,12 @@
(println c))
(when-not dry-run
(apply t/shell c)))))
(defn-spec execute-out! string?
[command ::core/command
config ::core/execution]
(let [{:keys [dry-run debug]} config]
(when debug
(println command))
(when-not dry-run
(:out (t/shell {:out :string} (clojure.string/join " " command))))))

View file

@ -0,0 +1,106 @@
(ns dda.backup.cred-rot.domain-test
(:require
[clojure.test :refer [deftest is]]
[clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as st]
[cljc.java-time.local-date :as ld]
[dda.backup.cred-rot.domain :as cut]))
(deftest test-spec-id
(is (s/valid? ::cut/id "521e0760"))
(is (s/valid? ::cut/id "test"))
(is (s/valid? ::cut/id "123456"))
(is (not (s/valid? ::cut/id "ROOT")))
(is (not (s/valid? ::cut/id "Test!"))))
(deftest test-spec-username
(is (s/valid? ::cut/userName "521e0760"))
(is (s/valid? ::cut/userName "Testuser"))
(is (s/valid? ::cut/userName "root"))
(is (s/valid? ::cut/userName "ROOT"))
(is (not (s/valid? ::cut/userName "test-user"))))
(deftest test-spec-hostName
(let [valid #(s/valid? ::cut/hostName %)]
(is (valid "test-some-combination-2"))
(is (valid "backup-restore-65bd9b6ff5-z69sn"))))
(deftest test-spec-timestamp
(let [valid #(s/valid? cut/timestamp-string? %)]
(is (valid "2024-10-18 13:08:16"))
(is (valid "2032-09-01 12:56:59"))
(is (not (valid "2024-13-5 13:08:16")))
(is (not (valid "2024-6-42 13:08:16")))
(is (not (valid "test")))))
(deftest test-parse-response
(is (=
(ld/parse "2024-10-19 13:16:54" cut/timestamp-formatter)
(:created
(first
(cut/parse-response "[
{
\"current\": false,
\"id\": \"521e0760\",
\"userName\": \"root\",
\"hostName\": \"backup-restore-65bd9b6ff5-z69sn\",
\"created\": \"2024-11-18 13:08:16\"
},
{
\"current\": true,
\"id\": \"b67161fb\",
\"userName\": \"root\",
\"hostName\": \"backup-restore-65bd9b6ff5-z69sn\",
\"created\": \"2024-10-19 13:16:54\"
}
]"))))))
(deftest test-set-new-password-action
(is (= :error-parse-password
(cut/set-new-password-action
(ld/parse "2024-10-19 13:16:54" cut/timestamp-formatter)
[]
{:new-password-config {:new-password-file "new-pw-file"
:valid-from "2024-11-29 12:00:16"}})))
(is (= :wait-for-new-pwd
(cut/set-new-password-action
(ld/parse "2024-10-19 13:16:54" cut/timestamp-formatter)
[{:current true
:id "a1"
:userName "root"
:hostName "host"
:created (ld/parse "2023-01-01 00:00:00" cut/timestamp-formatter)}]
{:new-password-config {:new-password-file "new-pw-file"
:valid-from "2024-11-29 12:00:16"}})))
(is (= :change-pwd
(cut/set-new-password-action
(ld/parse "2024-11-29 13:16:54" cut/timestamp-formatter)
[{:current true
:id "a1"
:userName "root"
:hostName "host"
:created (ld/parse "2023-01-01 00:00:00" cut/timestamp-formatter)}]
{:new-password-config {:new-password-file "new-pw-file"
:valid-from "2024-11-29 12:00:16"}})))
(is (= :pwd-change-finished
(cut/set-new-password-action
(ld/parse "2024-11-29 13:16:55" cut/timestamp-formatter)
[{:current true
:id "a2"
:userName "root"
:hostName "host"
:created (ld/parse "2024-11-29 13:16:54" cut/timestamp-formatter)}]
{:new-password-config {:new-password-file "new-pw-file"
:valid-from "2024-11-29 12:00:16"}})))
(is (= :no-pwd-change-needed
(cut/set-new-password-action
(ld/parse "2024-11-29 13:16:55" cut/timestamp-formatter)
[{:current true
:id "a2"
:userName "root"
:hostName "host"
:created (ld/parse "2024-11-29 13:16:54" cut/timestamp-formatter)}]
{})))
)