Skip to content

Commit e2bfba3

Browse files
Merge pull request #444 from step-security/cherry/armour
Add Armour
2 parents 0da07db + 1be8ea8 commit e2bfba3

File tree

15 files changed

+810
-487
lines changed

15 files changed

+810
-487
lines changed

.github/workflows/int.yml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ on:
77
- int
88

99
permissions: read-all
10-
10+
env:
11+
GOPRIVATE: github.com/step-security
1112
jobs:
1213
integration-test:
1314
permissions:
@@ -22,7 +23,19 @@ jobs:
2223
- name: Set up Go
2324
uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4
2425
with:
25-
go-version: 1.19
26+
go-version: 1.24.1
27+
28+
- name: Configure .netrc
29+
run: |
30+
if [[ ! -e "~/.netrc" ]]; then
31+
touch ~/.netrc
32+
fi
33+
printf "machine github.com login stepsecurity-infra-bot password ${{ secrets.PAT }}" >>~/.netrc
34+
35+
- name: Create go vendor dir
36+
run: |
37+
go mod vendor
38+
2639
- run: sudo go test -v
2740
- run: go build -ldflags="-s -w" -o ./agent
2841
- name: Configure aws credentials

.github/workflows/release.yml

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ on:
66
- '*'
77

88
permissions: read-all
9-
9+
env:
10+
GOPRIVATE: github.com/step-security
1011
jobs:
1112
release:
1213
permissions:
1314
contents: write
14-
runs-on: ubuntu-20.04
15+
runs-on: ubuntu-22.04
1516
steps:
1617
- uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
1718
with:
@@ -28,7 +29,21 @@ jobs:
2829
- name: Set up Go
2930
uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4
3031
with:
31-
go-version: 1.19
32+
go-version: 1.24.1
33+
34+
- name: Configure .netrc
35+
run: |
36+
if [[ ! -e "~/.netrc" ]]; then
37+
touch ~/.netrc
38+
fi
39+
printf "machine github.com login stepsecurity-infra-bot password ${{ secrets.PAT }}" >>~/.netrc
40+
41+
42+
- name: Create go vendor dir
43+
run: |
44+
go mod vendor
45+
46+
3247
- uses: goreleaser/goreleaser-action@5df302e5e9e4c66310a6b6493a8865b12c555af2
3348
with:
3449
distribution: goreleaser

.github/workflows/test.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ on:
77

88
permissions: read-all
99

10+
env:
11+
GOPRIVATE: github.com/step-security
12+
1013
jobs:
1114
test:
1215
permissions:
@@ -18,7 +21,19 @@ jobs:
1821
- name: Set up Go
1922
uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4
2023
with:
21-
go-version: 1.19
24+
go-version: 1.24.1
25+
26+
- name: Configure .netrc
27+
run: |
28+
if [[ ! -e "~/.netrc" ]]; then
29+
touch ~/.netrc
30+
fi
31+
printf "machine github.com login stepsecurity-infra-bot password ${{ secrets.PAT }}" >>~/.netrc
32+
33+
- name: Create go vendor dir
34+
run: |
35+
go mod vendor
36+
2237
- name: Run coverage
2338
run: sudo CI=true go test -race -coverprofile=coverage.txt -covermode=atomic
2439
- uses: codecov/codecov-action@40a12dcee2df644d47232dde008099a3e9e4f865

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@
1616
pkg/.DS_Store
1717
coverage.txt
1818

19-
.vscode/
19+
.vscode/
20+
21+
vendor
22+
private-src

agent.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"time"
1010

1111
"github.com/florianl/go-nflog/v2"
12+
"github.com/step-security/agent/lockfile"
13+
"github.com/step-security/armour/armour"
1214
)
1315

1416
const (
@@ -75,12 +77,25 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer,
7577
WriteLog(fmt.Sprintf("%s %s", StepSecurityLogCorrelationPrefix, config.CorrelationId))
7678
WriteLog("\n")
7779

80+
InitGlobalFeatureFlags(config.APIURL, apiclient)
81+
WriteLog("initialized global feature flags")
82+
WriteLog("\n")
83+
if IsArmourEnabled() {
84+
lf := lockfile.New(agentLockFile)
85+
if err := lf.TryLock(); err != nil {
86+
WriteLog("[agent] instance is already running")
87+
os.Exit(0)
88+
}
89+
defer lf.MustUnlock()
90+
}
91+
7892
// if this is a private repo
7993
if config.Private {
8094
isActive := apiclient.getSubscriptionStatus(config.Repo)
8195
if !isActive {
8296
config.EgressPolicy = EgressPolicyAudit
8397
config.DisableSudo = false
98+
config.DisableSudoAndContainers = false
8499
apiclient.DisableTelemetry = true
85100
config.DisableFileMonitoring = true
86101
WriteAnnotation("StepSecurity Harden Runner is disabled. A subscription is required for private repositories. Please start a free trial at https://www.stepsecurity.io")
@@ -119,6 +134,10 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer,
119134
sudo := Sudo{}
120135
var ipAddressEndpoints []ipAddressEndpoint
121136

137+
if config.DisableSudoAndContainers {
138+
go sudo.uninstallDocker()
139+
}
140+
122141
// hydrate dns cache
123142
if config.EgressPolicy == EgressPolicyBlock {
124143
for domainName, endpoints := range allowedEndpoints {
@@ -204,6 +223,44 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer,
204223
go refreshDNSEntries(ctx, iptables, allowedEndpoints, &dnsProxy)
205224
}
206225

226+
if IsArmourEnabled() {
227+
WriteLog("Armour is enabled")
228+
conf := &armour.Config{
229+
Pids: getPidsOfInterest(),
230+
Files: []string{},
231+
EnforceReadBlock: false,
232+
ApiConf: &armour.ApiConf{
233+
APIURL: config.APIURL,
234+
Repo: config.Repo,
235+
CorrelationID: config.CorrelationId,
236+
OneTimeKey: config.OneTimeKey,
237+
DisableTelemetry: config.DisableTelemetry,
238+
},
239+
}
240+
241+
conf.Files = append(conf.Files, getProcFilesOfInterest()...)
242+
243+
conf.Files = append(conf.Files, getFilesOfInterest()...)
244+
245+
mArmour := armour.NewArmour(ctx, conf)
246+
err := mArmour.Attach()
247+
if err != nil {
248+
WriteLog("Armour attachment failed")
249+
} else {
250+
defer mArmour.Detach()
251+
WriteLog("Armour attached")
252+
}
253+
}
254+
255+
if config.DisableSudoAndContainers {
256+
err := sudo.disableSudoAndContainers(tempDir)
257+
if err != nil {
258+
WriteLog(fmt.Sprintf("%s Unable to disable sudo and docker %v", StepSecurityAnnotationPrefix, err))
259+
} else {
260+
WriteLog("disabled sudo and docker")
261+
}
262+
}
263+
207264
if config.DisableSudo {
208265
err := sudo.disableSudo(tempDir)
209266
if err != nil {

apiclient.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"net/http"
89
"time"
910
)
@@ -104,6 +105,36 @@ func (apiclient *ApiClient) getSubscriptionStatus(repo string) bool {
104105
return true
105106
}
106107

108+
func (apiclient *ApiClient) getGlobalFeatureFlags() GlobalFeatureFlags {
109+
110+
url := fmt.Sprintf("%s/global-feature-flags?agent_type=%s", apiclient.APIURL, AgentTypeGitHubHosted)
111+
112+
req, err := http.NewRequest(http.MethodGet, url, nil)
113+
114+
if err != nil {
115+
return GlobalFeatureFlags{}
116+
}
117+
118+
resp, err := apiclient.Client.Do(req)
119+
120+
if err != nil {
121+
return GlobalFeatureFlags{}
122+
}
123+
124+
body, err := io.ReadAll(resp.Body)
125+
if err != nil {
126+
return GlobalFeatureFlags{}
127+
}
128+
129+
var globalFeatureFlags GlobalFeatureFlags
130+
err = json.Unmarshal(body, &globalFeatureFlags)
131+
if err != nil {
132+
return GlobalFeatureFlags{}
133+
}
134+
135+
return globalFeatureFlags
136+
}
137+
107138
func (apiclient *ApiClient) sendApiRequest(method, url string, body interface{}) error {
108139

109140
jsonData, _ := json.Marshal(body)

common.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
const (
12+
agentLockFile = "agent.lock"
13+
)
14+
15+
func getPidsOfInterest() []uint32 {
16+
out := []uint32{}
17+
18+
// our process
19+
out = append(out, uint32(os.Getpid()))
20+
21+
// systemd-resolved
22+
systemdResolvePid, _ := pidOf("systemd-resolved")
23+
24+
out = append(out, uint32(systemdResolvePid))
25+
26+
return out
27+
}
28+
29+
func getFilesOfInterest() []string {
30+
out := []string{}
31+
32+
// sudoers file
33+
out = append(out, "/etc/sudoers.d/runner")
34+
35+
// resolved.conf
36+
out = append(out, "/etc/resolv.conf")
37+
38+
// /etc/systemd/resolved.conf
39+
out = append(out, "/etc/systemd/resolved.conf")
40+
41+
// /etc/docker/daemon.json
42+
out = append(out, "/etc/docker/daemon.json")
43+
44+
return out
45+
}
46+
47+
func getProcFilesOfInterest() []string {
48+
out := []string{}
49+
50+
// our memory files
51+
out = append(out, getProcMemFiles(uint64(os.Getpid()))...)
52+
53+
// runner worker memory files
54+
runnerWorker, _ := pidOf("Runner.Worker")
55+
out = append(out, getProcMemFiles(runnerWorker)...)
56+
57+
// runner listener memory files
58+
runnerListener, _ := pidOf("Runner.Listener")
59+
out = append(out, getProcMemFiles(runnerListener)...)
60+
61+
return out
62+
}
63+
64+
func pidOf(procName string) (uint64, error) {
65+
66+
cmd := exec.Command("pidof", procName)
67+
68+
out, err := cmd.Output()
69+
if err != nil {
70+
return 0, err
71+
}
72+
73+
if len(out) == 0 {
74+
return 0, fmt.Errorf("no process exists")
75+
}
76+
77+
parts := strings.Fields(string(out))
78+
79+
num, err := strconv.Atoi(parts[0])
80+
if err != nil {
81+
return 0, err
82+
}
83+
84+
return uint64(num), nil
85+
86+
}
87+
88+
func getProcMemFiles(pid uint64) []string {
89+
90+
out := []string{}
91+
92+
if pid == 0 {
93+
return out
94+
}
95+
96+
out = []string{
97+
fmt.Sprintf("/proc/%d/maps", pid),
98+
fmt.Sprintf("/proc/%d/mem", pid),
99+
}
100+
101+
return out
102+
}

0 commit comments

Comments
 (0)