-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtemplate-ssh.txt
More file actions
271 lines (238 loc) · 10.7 KB
/
Copy pathtemplate-ssh.txt
File metadata and controls
271 lines (238 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
AWSTemplateFormatVersion: '2010-09-09'
Description: Launch node from a public AMI (wallet key only; SSH enabled via selected KeyPair + SSM)
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label: { default: "Node settings" }
Parameters: [WalletPubKey, BaseRpcUrl, InstanceType]
- Label: { default: "Access & Network" }
Parameters: [KeyName, VpcId, SubnetId]
ParameterLabels:
WalletPubKey: { default: "Wallet public key (Base address)" }
BaseRpcUrl: { default: "Base RPC URL" }
InstanceType: { default: "Instance type (free-tier friendly)" }
KeyName: { default: "SSH Key Pair" }
VpcId: { default: "VPC" }
SubnetId: { default: "Public Subnet (must have internet gateway)" }
Parameters:
WalletPubKey:
Type: String
Description: Base wallet address for receiving payments (e.g., 0x...)
AllowedPattern: "^0x[a-fA-F0-9]{40}$"
ConstraintDescription: Must be a valid Ethereum address starting with 0x
BaseRpcUrl:
Type: String
Description: Base RPC URL for blockchain connectivity (e.g., https://mainnet.base.org)
AllowedPattern: "^https?://.*"
ConstraintDescription: Must be a valid HTTP or HTTPS URL
InstanceType:
Type: String
Default: t3.micro
AllowedValues: [t3.micro, t2.micro]
Description: Default is t3.micro; pick t2.micro if t3.micro isn't offered.
KeyName:
Type: AWS::EC2::KeyPair::KeyName
Description: Pick an existing EC2 Key Pair for SSH (dropdown)
VpcId:
Type: AWS::EC2::VPC::Id
Description: Select your VPC
SubnetId:
Type: AWS::EC2::Subnet::Id
Description: Choose a PUBLIC subnet with internet gateway AND auto-assign public IP enabled
Mappings:
RegionMap:
ca-central-1: { Ami: ami-091b2ab27cd7dc12f }
Resources:
RequestAMIAccess:
Type: Custom::RequestAccess
Properties:
ServiceToken: arn:aws:lambda:ca-central-1:152547424026:function:GrantAMIAccess
RequestingAccountId: !Ref AWS::AccountId
AmiId: !FindInMap [RegionMap, !Ref "AWS::Region", Ami]
NodeSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Node SG (HTTP/4000/22 open)
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
Description: SSH access
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Description: HTTP access
- IpProtocol: tcp
FromPort: 4000
ToPort: 4000
CidrIp: 0.0.0.0/0
Description: Node API access
InstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal: { Service: ec2.amazonaws.com }
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles: [ !Ref InstanceRole ]
NodeInstance:
Type: AWS::EC2::Instance
DependsOn: RequestAMIAccess
Properties:
ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", Ami]
InstanceType: !Ref InstanceType
KeyName: !Ref KeyName
IamInstanceProfile: !Ref InstanceProfile
SubnetId: !Ref SubnetId
SecurityGroupIds: [ !Ref NodeSG ]
MetadataOptions:
HttpEndpoint: enabled
HttpTokens: required
HttpPutResponseHopLimit: 1
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs: { VolumeType: gp3, VolumeSize: 20, Encrypted: true, DeleteOnTermination: true }
UserData:
Fn::Base64:
Fn::Sub: |
#cloud-config
users:
- name: admin
system: false
shell: /bin/bash
home: /home/admin
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
write_files:
- path: /home/admin/starter-example-nodes/education-directory/.env
owner: admin:admin
permissions: '0644'
content: |
PAYMENT_WALLET_ADDRESS=${WalletPubKey}
BASE_RPC_URL=${BaseRpcUrl}
- path: /home/admin/restart-node.sh
owner: admin:admin
permissions: '0755'
content: |
#!/bin/bash
set -euo pipefail
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a /var/log/node-restart.log; }
log "Starting node restart process..."
log "Waiting for Docker daemon..."
timeout 300 bash -c 'until docker info > /dev/null 2>&1; do sleep 2; done' || { log "ERROR: Docker daemon not ready after 5 minutes"; exit 1; }
log "Docker daemon is ready"
if [[ ! -d "/home/admin/starter-example-nodes/education-directory" ]]; then log "ERROR: Expected directory does not exist"; exit 1; fi
cd /home/admin/starter-example-nodes/education-directory
run_as_admin() { sudo -u admin -g admin bash -c "cd /home/admin/starter-example-nodes/education-directory && $1"; }
log "Pulling latest Docker images..."; if ! run_as_admin "docker compose pull"; then log "ERROR: Failed to pull Docker images"; exit 1; fi
log "Stopping existing containers..."; run_as_admin "docker compose down" || log "No existing containers to stop"
sleep 5
log "Starting containers..."; if ! run_as_admin "docker compose up -d"; then log "ERROR: Failed to start containers"; exit 1; fi
log "Waiting for services to start..."; sleep 15
if ! run_as_admin "docker compose ps --services --filter status=running" | grep -q .; then log "ERROR: No containers are running after startup"; run_as_admin "docker compose logs" || true; exit 1; fi
log "Verifying node health and payment configuration..."
for attempt in {1..30}; do
if response=$(curl -s http://localhost:4000/api/info 2>/dev/null); then
if echo "$response" | jq -e '.brain.payment' > /dev/null 2>&1; then
log "SUCCESS: Node is healthy and payment is configured"
log "Payment address: $(echo "$response" | jq -r '.brain.payment // "unknown"')"
log "Node restart completed successfully"; exit 0
else
log "Attempt $attempt: Node running but payment not configured yet"
fi
else
log "Attempt $attempt: Node not responding to health check"
fi
if [ $attempt -eq 30 ]; then
log "ERROR: Node failed to configure payment after 30 attempts (5 minutes)"
log "Final containers status:"; run_as_admin "docker compose ps" || true
log "Final container logs:"; run_as_admin "docker compose logs --tail=50" || true
exit 1
fi
sleep 10
done
- path: /etc/systemd/system/node-restart.service
permissions: '0644'
content: |
[Unit]
Description=Restart node with new configuration
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/home/admin/restart-node.sh
User=root
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
runcmd:
# ensure admin home and .ssh directories exist
- mkdir -p /home/admin/.ssh
- chmod 700 /home/admin/.ssh
- chown -R admin:admin /home/admin
# add admin to common admin groups cross-distro
- 'usermod -aG wheel admin || true'
- 'usermod -aG sudo admin || true'
- 'usermod -aG docker admin || true'
- 'rm -f /home/admin/starter-example-nodes/education-directory/data/signing.pem'
# Configure SSH key for admin user
- |
set -euo pipefail
echo "Setting up SSH key for admin user..."
# Try to get public key using IMDSv2 first, fall back to IMDSv1
META="http://169.254.169.254/latest"
# Try IMDSv2 (session token)
TOKEN=$(curl -s -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 21600' "$META/api/token" 2>/dev/null || echo "")
if [ -n "$TOKEN" ]; then
echo "Using IMDSv2..."
PUBLIC_KEY=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" "$META/meta-data/public-keys/0/openssh-key" 2>/dev/null || echo "")
else
echo "IMDSv1 fallback..."
PUBLIC_KEY=$(curl -s "$META/meta-data/public-keys/0/openssh-key" 2>/dev/null || echo "")
fi
if [ -n "$PUBLIC_KEY" ]; then
echo "$PUBLIC_KEY" > /home/admin/.ssh/authorized_keys
chmod 600 /home/admin/.ssh/authorized_keys
chown admin:admin /home/admin/.ssh/authorized_keys
echo "SUCCESS: SSH key configured for admin user"
echo "Key fingerprint: $(ssh-keygen -lf /home/admin/.ssh/authorized_keys 2>/dev/null || echo 'unable to get fingerprint')"
else
echo "ERROR: Could not retrieve public key from EC2 metadata service"
exit 1
fi
# Start service
- systemctl daemon-reload
- systemctl enable node-restart.service
- systemctl start node-restart.service
Outputs:
InstanceId:
Value: !Ref NodeInstance
Description: Launched instance ID
WalletAddress:
Value: !Ref WalletPubKey
Description: Configured payment wallet address
BaseRpcUrl:
Value: !Ref BaseRpcUrl
Description: Configured Base RPC URL
HealthCheckURL:
Value: !Sub "http://${NodeInstance.PublicIp}:4000/api/health"
Description: Node health check endpoint
InfoURL:
Value: !Sub "http://${NodeInstance.PublicIp}:4000/api/info"
Description: Node info endpoint (verify payment configuration)
SSHConnection:
Value: !Sub "ssh admin@${NodeInstance.PublicIp}"
Description: SSH connection command
SSMSessionCommand:
Value: !Sub "aws ssm start-session --target ${NodeInstance}"
Description: Alternative access via SSM Session Manager